# 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 if fields.include?('LABELS') fields[fields.index('LABELS')] = X_GM_LABELS end 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 if !new_labels.include?("\\Inbox") new_labels << "\\Inbox" end 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]) if !fetched raise "Thread not found for UID #{uid}!" end 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, ["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 do |mb| (mb.attr & [:Drafts, :Sent, :Junk, :Flagged, :Trash]).any? end.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