225 lines
7.0 KiB
Ruby
225 lines
7.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Imap
|
|
module Providers
|
|
# Gmail has a special header for both labels (X-GM-LABELS) and their
|
|
# threading system (X-GM-THRID). We need to monkey-patch Net::IMAP to
|
|
# get access to these. Also the archiving functionality is custom,
|
|
# all UIDs in a thread must have the \\Inbox label removed.
|
|
#
|
|
class Gmail < Generic
|
|
X_GM_LABELS = "X-GM-LABELS"
|
|
X_GM_THRID = "X-GM-THRID"
|
|
|
|
def imap
|
|
@imap ||= super.tap { |imap| apply_gmail_patch(imap) }
|
|
end
|
|
|
|
def emails(uids, fields, opts = {})
|
|
# gmail has a special header for labels
|
|
fields[fields.index("LABELS")] = X_GM_LABELS if fields.include?("LABELS")
|
|
|
|
emails = super(uids, fields, opts)
|
|
|
|
emails.each do |email|
|
|
email["LABELS"] = Array(email["LABELS"])
|
|
|
|
if email[X_GM_LABELS]
|
|
email["LABELS"] << Array(email.delete(X_GM_LABELS))
|
|
email["LABELS"].flatten!
|
|
end
|
|
|
|
email["LABELS"] << '\\Inbox' if @open_mailbox_name == "INBOX"
|
|
|
|
email["LABELS"].uniq!
|
|
end
|
|
|
|
emails
|
|
end
|
|
|
|
def store(uid, attribute, old_set, new_set)
|
|
attribute = X_GM_LABELS if attribute == "LABELS"
|
|
super(uid, attribute, old_set, new_set)
|
|
end
|
|
|
|
def to_tag(label)
|
|
# Label `\\Starred` is Gmail equivalent of :Flagged (both present)
|
|
return "starred" if label == :Flagged
|
|
return if label == "[Gmail]/All Mail"
|
|
|
|
label = label.to_s.gsub("[Gmail]/", "")
|
|
super(label)
|
|
end
|
|
|
|
def tag_to_flag(tag)
|
|
return :Flagged if tag == "starred"
|
|
|
|
super(tag)
|
|
end
|
|
|
|
def tag_to_label(tag)
|
|
return '\\Important' if tag == "important"
|
|
return '\\Starred' if tag == "starred"
|
|
|
|
super(tag)
|
|
end
|
|
|
|
# All emails in the thread must be archived in Gmail for the thread
|
|
# to get removed from the inbox
|
|
def archive(uid)
|
|
thread_id = thread_id_from_uid(uid)
|
|
emails_to_archive = emails_in_thread(thread_id)
|
|
emails_to_archive.each do |email|
|
|
labels = email["LABELS"]
|
|
new_labels = labels.reject { |l| l == "\\Inbox" }
|
|
store(email["UID"], "LABELS", labels, new_labels)
|
|
end
|
|
ImapSyncLog.log(
|
|
"Thread ID #{thread_id} (UID #{uid}) archived in Gmail mailbox for #{@username}",
|
|
:debug,
|
|
)
|
|
end
|
|
|
|
# Though Gmail considers the email thread unarchived if the first email
|
|
# has the \\Inbox label applied, we want to do this to all emails in the
|
|
# thread to be consistent with archive behaviour.
|
|
def unarchive(uid)
|
|
thread_id = thread_id_from_uid(uid)
|
|
emails_to_unarchive = emails_in_thread(thread_id)
|
|
emails_to_unarchive.each do |email|
|
|
labels = email["LABELS"]
|
|
new_labels = labels.dup
|
|
new_labels << "\\Inbox" if !new_labels.include?("\\Inbox")
|
|
store(email["UID"], "LABELS", labels, new_labels)
|
|
end
|
|
ImapSyncLog.log(
|
|
"Thread ID #{thread_id} (UID #{uid}) unarchived in Gmail mailbox for #{@username}",
|
|
:debug,
|
|
)
|
|
end
|
|
|
|
def thread_id_from_uid(uid)
|
|
fetched = imap.uid_fetch(uid, [X_GM_THRID])
|
|
raise "Thread not found for UID #{uid}!" if !fetched
|
|
|
|
fetched.last.attr[X_GM_THRID]
|
|
end
|
|
|
|
def emails_in_thread(thread_id)
|
|
uids_to_fetch = imap.uid_search("#{X_GM_THRID} #{thread_id}")
|
|
emails(uids_to_fetch, %w[UID LABELS])
|
|
end
|
|
|
|
def trash_move(uid)
|
|
thread_id = thread_id_from_uid(uid)
|
|
email_uids_to_trash = emails_in_thread(thread_id).map { |e| e["UID"] }
|
|
|
|
imap.uid_move(email_uids_to_trash, trash_mailbox)
|
|
ImapSyncLog.log(
|
|
"Thread ID #{thread_id} (UID #{uid}) trashed in Gmail mailbox for #{@username}",
|
|
:debug,
|
|
)
|
|
{ trash_uid_validity: open_trash_mailbox, email_uids_to_trash: email_uids_to_trash }
|
|
end
|
|
|
|
# Some mailboxes are just not useful or advisable to sync with. This is
|
|
# used for the dropdown in the UI where we allow the user to select the
|
|
# IMAP mailbox to sync with.
|
|
def filter_mailboxes(mailboxes_with_attributes)
|
|
mailboxes_with_attributes
|
|
.reject { |mb| (mb.attr & %i[Drafts Sent Junk Flagged Trash]).any? }
|
|
.map(&:name)
|
|
end
|
|
|
|
private
|
|
|
|
def apply_gmail_patch(imap)
|
|
class << imap.instance_variable_get("@parser")
|
|
# Modified version of the original `msg_att` from here:
|
|
# https://github.com/ruby/ruby/blob/1cc8ff001da217d0e98d13fe61fbc9f5547ef722/lib/net/imap.rb#L2346
|
|
#
|
|
# This is done so we can extract X-GM-LABELS, X-GM-MSGID, and
|
|
# X-GM-THRID, all Gmail extended attributes.
|
|
#
|
|
# rubocop:disable Style/RedundantReturn
|
|
def msg_att(n)
|
|
match(T_LPAR)
|
|
attr = {}
|
|
while true
|
|
token = lookahead
|
|
case token.symbol
|
|
when T_RPAR
|
|
shift_token
|
|
break
|
|
when T_SPACE
|
|
shift_token
|
|
next
|
|
end
|
|
case token.value
|
|
when /\A(?:ENVELOPE)\z/ni
|
|
name, val = envelope_data
|
|
when /\A(?:FLAGS)\z/ni
|
|
name, val = flags_data
|
|
when /\A(?:INTERNALDATE)\z/ni
|
|
name, val = internaldate_data
|
|
when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
|
|
name, val = rfc822_text
|
|
when /\A(?:RFC822\.SIZE)\z/ni
|
|
name, val = rfc822_size
|
|
when /\A(?:BODY(?:STRUCTURE)?)\z/ni
|
|
name, val = body_data
|
|
when /\A(?:UID)\z/ni
|
|
name, val = uid_data
|
|
when /\A(?:MODSEQ)\z/ni
|
|
name, val = modseq_data
|
|
|
|
# Adding support for GMail extended attributes.
|
|
when /\A(?:X-GM-LABELS)\z/ni
|
|
name, val = label_data
|
|
when /\A(?:X-GM-MSGID)\z/ni
|
|
name, val = uid_data
|
|
when /\A(?:X-GM-THRID)\z/ni
|
|
name, val = uid_data
|
|
# End custom support for Gmail.
|
|
else
|
|
parse_error("unknown attribute `%s' for {%d}", token.value, n)
|
|
end
|
|
attr[name] = val
|
|
end
|
|
return attr
|
|
end
|
|
|
|
def label_data
|
|
token = match(T_ATOM)
|
|
name = token.value.upcase
|
|
|
|
match(T_SPACE)
|
|
match(T_LPAR)
|
|
|
|
result = []
|
|
while true
|
|
token = lookahead
|
|
case token.symbol
|
|
when T_RPAR
|
|
shift_token
|
|
break
|
|
when T_SPACE
|
|
shift_token
|
|
end
|
|
|
|
token = lookahead
|
|
if string_token?(token)
|
|
result.push(string)
|
|
else
|
|
result.push(atom)
|
|
end
|
|
end
|
|
return name, result
|
|
end
|
|
# rubocop:enable Style/RedundantReturn
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|