mirror of
https://github.com/discourse/discourse.git
synced 2025-02-24 22:45:27 +00:00
Adds functionality to reflect topic delete in Discourse to IMAP inbox (Gmail only for now) and reflecting Gmail deletes in Discourse. Adding lots of tests, various refactors and code improvements. When Discourse topic is destroyed in PostDestroyer mark the topic incoming email as imap_sync: true, and do the opposite when post is recovered.
216 lines
6.6 KiB
Ruby
216 lines
6.6 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
|
|
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
|
|
Imap::Sync::Logger.log("[IMAP] Thread ID #{thread_id} (UID #{uid}) archived in Gmail mailbox for #{@username}")
|
|
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
|
|
Imap::Sync::Logger.log("[IMAP] Thread ID #{thread_id} (UID #{uid}) unarchived in Gmail mailbox for #{@username}")
|
|
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)
|
|
Imap::Sync::Logger.log("[IMAP] Thread ID #{thread_id} (UID #{uid}) trashed in Gmail mailbox for #{@username}")
|
|
{ trash_uid_validity: open_trash_mailbox, email_uids_to_trash: email_uids_to_trash }
|
|
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
|