Class: Imap::Providers::Generic

Inherits:
Object
  • Object
show all
Defined in:
lib/imap/providers/generic.rb

Direct Known Subclasses

Gmail

Instance Method Summary collapse

Constructor Details

#initialize(server, options = {}) ⇒ Generic

Returns a new instance of Generic.



28
29
30
31
32
33
34
35
# File 'lib/imap/providers/generic.rb', line 28

def initialize(server, options = {})
  @server = server
  @port = options[:port] || 993
  @ssl = options[:ssl] || true
  @username = options[:username]
  @password = options[:password]
  @timeout = options[:timeout] || 10
end

Instance Method Details

#account_digestObject



37
38
39
# File 'lib/imap/providers/generic.rb', line 37

def 
  @account_digest ||= Digest::MD5.hexdigest("#{@username}:#{@server}")
end

#archive(uid) ⇒ Object



182
183
184
# File 'lib/imap/providers/generic.rb', line 182

def archive(uid)
  # do nothing by default, just removing the Inbox label should be enough
end

#can?(capability) ⇒ Boolean

Returns:

  • (Boolean)


62
63
64
65
# File 'lib/imap/providers/generic.rb', line 62

def can?(capability)
  @capabilities ||= imap.responses["CAPABILITY"][-1] || imap.capability
  @capabilities.include?(capability)
end

#connect!Object



49
50
51
# File 'lib/imap/providers/generic.rb', line 49

def connect!
  imap.(@username, @password)
end

#disconnect!Object



53
54
55
56
57
58
59
60
# File 'lib/imap/providers/generic.rb', line 53

def disconnect!
  begin
    imap.logout
  rescue StandardError
    nil
  end
  imap.disconnect
end

#disconnected?Boolean

Returns:

  • (Boolean)


45
46
47
# File 'lib/imap/providers/generic.rb', line 45

def disconnected?
  @imap && @imap.disconnected?
end

#emails(uids, fields, opts = {}) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/imap/providers/generic.rb', line 110

def emails(uids, fields, opts = {})
  fetched = imap.uid_fetch(uids, fields)

  # This will happen if the email does not exist in the provided mailbox.
  # It may have been deleted or otherwise moved, e.g. if deleted in Gmail
  # it will end up in "[Gmail]/Bin"
  return [] if fetched.nil?

  fetched.map do |email|
    attributes = {}

    fields.each { |field| attributes[field] = email.attr[field] }

    attributes
  end
end

#filter_mailboxes(mailboxes) ⇒ Object



176
177
178
179
180
# File 'lib/imap/providers/generic.rb', line 176

def filter_mailboxes(mailboxes)
  # we do not want to filter out any mailboxes for generic providers,
  # because we do not know what they are ahead of time
  mailboxes
end

#find_spam_by_message_ids(message_ids) ⇒ Object



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/imap/providers/generic.rb', line 262

def find_spam_by_message_ids(message_ids)
  spam_emails = []
  spam_uid_validity =
    open_spam_mailbox do
      spam_email_uids = find_uids_by_message_ids(message_ids)
      if spam_email_uids.any?
        spam_emails =
          emails(spam_email_uids, %w[UID ENVELOPE]).map do |e|
            BasicMail.new(
              message_id: Email::MessageIdService.message_id_clean(e["ENVELOPE"].message_id),
              uid: e["UID"],
            )
          end
      end
    end

  SpamMailResponse.new.tap do |resp|
    resp.spam_emails = spam_emails
    resp.spam_uid_validity = spam_uid_validity
  end
end

#find_trashed_by_message_ids(message_ids) ⇒ Object



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/imap/providers/generic.rb', line 240

def find_trashed_by_message_ids(message_ids)
  trashed_emails = []
  trash_uid_validity =
    open_trash_mailbox do
      trashed_email_uids = find_uids_by_message_ids(message_ids)
      if trashed_email_uids.any?
        trashed_emails =
          emails(trashed_email_uids, %w[UID ENVELOPE]).map do |e|
            BasicMail.new(
              message_id: Email::MessageIdService.message_id_clean(e["ENVELOPE"].message_id),
              uid: e["UID"],
            )
          end
      end
    end

  TrashedMailResponse.new.tap do |resp|
    resp.trashed_emails = trashed_emails
    resp.trash_uid_validity = trash_uid_validity
  end
end

#find_uids_by_message_ids(message_ids) ⇒ Object



284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/imap/providers/generic.rb', line 284

def find_uids_by_message_ids(message_ids)
  header_message_id_terms =
    message_ids.map do |msgid|
      "HEADER Message-ID '#{Email::MessageIdService.message_id_rfc_format(msgid)}'"
    end

  # OR clauses are written in Polish notation...so the query looks like this:
  # OR OR HEADER Message-ID XXXX HEADER Message-ID XXXX HEADER Message-ID XXXX
  or_clauses = "OR " * (header_message_id_terms.length - 1)
  query = "#{or_clauses}#{header_message_id_terms.join(" ")}"

  imap.uid_search(query)
end

#imapObject



41
42
43
# File 'lib/imap/providers/generic.rb', line 41

def imap
  @imap ||= Net::IMAP.new(@server, port: @port, ssl: @ssl, open_timeout: @timeout)
end

#labelsObject



79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/imap/providers/generic.rb', line 79

def labels
  @labels ||=
    begin
      labels = {}

      list_mailboxes.each do |name|
        if tag = to_tag(name)
          labels[tag] = name
        end
      end

      labels
    end
end

#list_mailboxes(attr_filter = nil) ⇒ Object



147
148
149
150
# File 'lib/imap/providers/generic.rb', line 147

def list_mailboxes(attr_filter = nil)
  # Lists all the mailboxes but just returns the names.
  list_mailboxes_with_attributes(attr_filter).map(&:name)
end

#list_mailboxes_with_attributes(attr_filter = nil) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/imap/providers/generic.rb', line 152

def list_mailboxes_with_attributes(attr_filter = nil)
  # Basically, list all mailboxes in the root of the server.
  # ref: https://tools.ietf.org/html/rfc3501#section-6.3.8
  imap
    .list("", "*")
    .reject do |m|
      # Noselect cannot be selected with the SELECT command.
      # technically we could use this for readonly mode when
      # SiteSetting.imap_write is disabled...maybe a later TODO
      # ref: https://tools.ietf.org/html/rfc3501#section-7.2.2
      m.attr.include?(:Noselect)
    end
    .select do |m|
      # There are Special-Use mailboxes denoted by an attribute. For
      # example, some common ones are \Trash or \Sent.
      # ref: https://tools.ietf.org/html/rfc6154
      if attr_filter
        m.attr.include? attr_filter
      else
        true
      end
    end
end

#open_mailbox(mailbox_name, write: false) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/imap/providers/generic.rb', line 94

def open_mailbox(mailbox_name, write: false)
  if write
    if !SiteSetting.enable_imap_write
      raise WriteDisabledError.new("Two-way IMAP sync is disabled! Cannot write to inbox.")
    end
    imap.select(mailbox_name)
  else
    imap.examine(mailbox_name)
  end

  @open_mailbox_name = mailbox_name
  @open_mailbox_write = write

  { uid_validity: imap.responses["UIDVALIDITY"][-1] }
end

#open_spam_mailbox(write: false) {|spam_uid_validity| ... } ⇒ Object

open the spam mailbox for inspection or writing. after the yield we close the spam and reopen the original mailbox to continue operations. the normal open_mailbox call can be made if more extensive spam ops need to be done.

Yields:

  • (spam_uid_validity)


228
229
230
231
232
233
234
235
236
237
238
# File 'lib/imap/providers/generic.rb', line 228

def open_spam_mailbox(write: false)
  open_mailbox_before_spam = @open_mailbox_name
  open_mailbox_before_spam_write = @open_mailbox_write

  spam_uid_validity = open_mailbox(spam_mailbox, write: write)[:uid_validity]

  yield(spam_uid_validity) if block_given?

  open_mailbox(open_mailbox_before_spam, write: open_mailbox_before_spam_write)
  spam_uid_validity
end

#open_trash_mailbox(write: false) {|trash_uid_validity| ... } ⇒ Object

open the trash mailbox for inspection or writing. after the yield we close the trash and reopen the original mailbox to continue operations. the normal open_mailbox call can be made if more extensive trash ops need to be done.

Yields:

  • (trash_uid_validity)


212
213
214
215
216
217
218
219
220
221
222
# File 'lib/imap/providers/generic.rb', line 212

def open_trash_mailbox(write: false)
  open_mailbox_before_trash = @open_mailbox_name
  open_mailbox_before_trash_write = @open_mailbox_write

  trash_uid_validity = open_mailbox(trash_mailbox, write: write)[:uid_validity]

  yield(trash_uid_validity) if block_given?

  open_mailbox(open_mailbox_before_trash, write: open_mailbox_before_trash_write)
  trash_uid_validity
end

#spam_mailboxObject

Look for the special Junk XLIST attribute.



200
201
202
203
204
205
206
# File 'lib/imap/providers/generic.rb', line 200

def spam_mailbox
  Discourse
    .cache
    .fetch("imap_spam_mailbox_#{}", expires_in: 30.minutes) do
      list_mailboxes(:Junk).first
    end
end

#store(uid, attribute, old_set, new_set) ⇒ Object



127
128
129
130
131
132
# File 'lib/imap/providers/generic.rb', line 127

def store(uid, attribute, old_set, new_set)
  additions = new_set.reject { |val| old_set.include?(val) }
  imap.uid_store(uid, "+#{attribute}", additions) if additions.length > 0
  removals = old_set.reject { |val| new_set.include?(val) }
  imap.uid_store(uid, "-#{attribute}", removals) if removals.length > 0
end

#tag_to_flag(tag) ⇒ Object



139
140
141
# File 'lib/imap/providers/generic.rb', line 139

def tag_to_flag(tag)
  :Seen if tag == "seen"
end

#tag_to_label(tag) ⇒ Object



143
144
145
# File 'lib/imap/providers/generic.rb', line 143

def tag_to_label(tag)
  tag
end

#to_tag(label) ⇒ Object



134
135
136
137
# File 'lib/imap/providers/generic.rb', line 134

def to_tag(label)
  label = DiscourseTagging.clean_tag(label.to_s)
  label if label != "inbox" && label != "sent"
end

#trash(uid) ⇒ Object



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/imap/providers/generic.rb', line 298

def trash(uid)
  # MOVE is way easier than doing the COPY \Deleted EXPUNGE dance ourselves.
  # It is supported by Gmail and Outlook.
  if can?("MOVE")
    trash_move(uid)
  else
    # default behaviour for IMAP servers is to add the \Deleted flag
    # then EXPUNGE the mailbox which permanently deletes these messages
    # https://tools.ietf.org/html/rfc3501#section-6.4.3
    #
    # TODO: We may want to add the option at some point to copy to some
    # other mailbox first before doing this (e.g. Trash)
    store(uid, "FLAGS", [], ["\\Deleted"])
    imap.expunge
  end
end

#trash_mailboxObject

Look for the special Trash XLIST attribute.



191
192
193
194
195
196
197
# File 'lib/imap/providers/generic.rb', line 191

def trash_mailbox
  Discourse
    .cache
    .fetch("imap_trash_mailbox_#{}", expires_in: 30.minutes) do
      list_mailboxes(:Trash).first
    end
end

#trash_move(uid) ⇒ Object



315
316
317
# File 'lib/imap/providers/generic.rb', line 315

def trash_move(uid)
  # up to the provider
end

#uids(opts = {}) ⇒ Object



67
68
69
70
71
72
73
74
75
76
77
# File 'lib/imap/providers/generic.rb', line 67

def uids(opts = {})
  if opts[:from] && opts[:to]
    imap.uid_search("UID #{opts[:from]}:#{opts[:to]}")
  elsif opts[:from]
    imap.uid_search("UID #{opts[:from]}:*")
  elsif opts[:to]
    imap.uid_search("UID 1:#{opts[:to]}")
  else
    imap.uid_search("ALL")
  end
end

#unarchive(uid) ⇒ Object



186
187
188
# File 'lib/imap/providers/generic.rb', line 186

def unarchive(uid)
  # same as above
end