rgrove-larch 1.0.0.8 → 1.0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. data/lib/larch/imap/mailbox.rb +276 -0
  2. metadata +2 -1
@@ -0,0 +1,276 @@
1
+ module Larch; class IMAP
2
+
3
+ # Represents an IMAP mailbox.
4
+ class Mailbox
5
+ attr_reader :attr, :delim, :imap, :name, :state
6
+
7
+ # Maximum number of messages to fetch at once.
8
+ MAX_FETCH_COUNT = 1024
9
+
10
+ # Regex to capture a Message-Id header.
11
+ REGEX_MESSAGE_ID = /message-id\s*:\s*(\S+)/i
12
+
13
+ # Minimum time (in seconds) allowed between mailbox scans.
14
+ SCAN_INTERVAL = 60
15
+
16
+ def initialize(imap, name, delim, *attr)
17
+ raise ArgumentError, "must provide a Larch::IMAP instance" unless imap.is_a?(Larch::IMAP)
18
+
19
+ @delim = delim
20
+ @imap = imap
21
+ @name = name
22
+ @attr = attr
23
+
24
+ @ids = {}
25
+ @last_id = 0
26
+ @last_scan = nil
27
+ @mutex = Mutex.new
28
+
29
+ # Valid mailbox states are :closed (no mailbox open), :examined (mailbox
30
+ # open and read-only), or :selected (mailbox open and read-write).
31
+ @state = :closed
32
+
33
+ # Create private convenience methods (debug, info, warn, etc.) to make
34
+ # logging easier.
35
+ Logger::LEVELS.each_key do |level|
36
+ Mailbox.class_eval do
37
+ define_method(level) do |msg|
38
+ Larch.log.log(level, "#{@imap.username}@#{@imap.host}: #{@name}: #{msg}")
39
+ end
40
+
41
+ private level
42
+ end
43
+ end
44
+ end
45
+
46
+ # Appends the specified Larch::IMAP::Message to this mailbox if it doesn't
47
+ # already exist. Returns +true+ if the message was appended successfully,
48
+ # +false+ if the message already exists in the mailbox.
49
+ def append(message)
50
+ raise ArgumentError, "must provide a Larch::IMAP::Message object" unless message.is_a?(Larch::IMAP::Message)
51
+ return false if has_message?(message)
52
+
53
+ @imap.safely do
54
+ imap_select(!!@imap.options[:create_mailbox])
55
+
56
+ debug "appending message: #{message.id}"
57
+ @imap.conn.append(@name, message.rfc822, message.flags, message.internaldate)
58
+ end
59
+
60
+ true
61
+ end
62
+ alias << append
63
+
64
+ # Iterates through Larch message ids in this mailbox, yielding each one to the
65
+ # provided block.
66
+ def each
67
+ scan
68
+ @ids.dup.each_key {|id| yield id }
69
+ end
70
+
71
+ # Gets a Net::IMAP::Envelope for the specified message id.
72
+ def envelope(message_id)
73
+ scan
74
+ raise Larch::IMAP::MessageNotFoundError, "message not found: #{message_id}" unless uid = @ids[message_id]
75
+
76
+ debug "fetching envelope: #{message_id}"
77
+ imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
78
+ end
79
+
80
+ # Fetches a Larch::IMAP::Message struct representing the message with the
81
+ # specified Larch message id.
82
+ def fetch(message_id, peek = false)
83
+ scan
84
+ raise Larch::IMAP::MessageNotFoundError, "message not found: #{message_id}" unless uid = @ids[message_id]
85
+
86
+ debug "#{peek ? 'peeking at' : 'fetching'} message: #{message_id}"
87
+ data = imap_uid_fetch([uid], [(peek ? 'BODY.PEEK[]' : 'BODY[]'), 'FLAGS', 'INTERNALDATE', 'ENVELOPE']).first
88
+
89
+ Message.new(message_id, data.attr['ENVELOPE'], data.attr['BODY[]'],
90
+ data.attr['FLAGS'], Time.parse(data.attr['INTERNALDATE']))
91
+ end
92
+ alias [] fetch
93
+
94
+ # Returns +true+ if a message with the specified Larch <em>message_id</em>
95
+ # exists in this mailbox, +false+ otherwise.
96
+ def has_message?(message_id)
97
+ scan
98
+ @ids.has_key?(message_id)
99
+ end
100
+
101
+ # Gets the number of messages in this mailbox.
102
+ def length
103
+ scan
104
+ @ids.length
105
+ end
106
+ alias size length
107
+
108
+ # Same as fetch, but doesn't mark the message as seen.
109
+ def peek(message_id)
110
+ fetch(message_id, true)
111
+ end
112
+
113
+ # Resets the mailbox state.
114
+ def reset
115
+ @mutex.synchronize { @state = :closed }
116
+ end
117
+
118
+ # Fetches message headers from this mailbox.
119
+ def scan
120
+ return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
121
+
122
+ begin
123
+ imap_examine
124
+ rescue Error => e
125
+ return if @imap.options[:create_mailbox]
126
+ raise
127
+ end
128
+
129
+ last_id = @imap.safely { @imap.conn.responses['EXISTS'].last }
130
+
131
+ @mutex.synchronize { @last_scan = Time.now }
132
+ return if last_id == @last_id
133
+
134
+ range = (@last_id + 1)..last_id
135
+
136
+ @mutex.synchronize { @last_id = last_id }
137
+
138
+ info "fetching message headers #{range}" <<
139
+ (@imap.options[:fast_scan] ? ' (fast scan)' : '')
140
+
141
+ fields = if @imap.options[:fast_scan]
142
+ ['UID', 'RFC822.SIZE', 'INTERNALDATE']
143
+ else
144
+ "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)] RFC822.SIZE INTERNALDATE)"
145
+ end
146
+
147
+ imap_fetch(range, fields).each do |data|
148
+ id = create_id(data)
149
+
150
+ unless uid = data.attr['UID']
151
+ error "UID not in IMAP response for message: #{id}"
152
+ next
153
+ end
154
+
155
+ if Larch.log.level == :debug && @ids.has_key?(id)
156
+ envelope = imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
157
+ debug "duplicate message? #{id} (Subject: #{envelope.subject})"
158
+ end
159
+
160
+ @mutex.synchronize { @ids[id] = uid }
161
+ end
162
+ end
163
+
164
+ private
165
+
166
+ # Creates an id suitable for uniquely identifying a specific message across
167
+ # servers (we hope).
168
+ #
169
+ # If the given message data includes a valid Message-Id header, then that will
170
+ # be used to generate an MD5 hash. Otherwise, the hash will be generated based
171
+ # on the message's RFC822.SIZE and INTERNALDATE.
172
+ def create_id(data)
173
+ ['RFC822.SIZE', 'INTERNALDATE'].each do |a|
174
+ raise Error, "required data not in IMAP response: #{a}" unless data.attr[a]
175
+ end
176
+
177
+ if data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]'] =~ REGEX_MESSAGE_ID
178
+ Digest::MD5.hexdigest($1)
179
+ else
180
+ Digest::MD5.hexdigest(sprintf('%d%d', data.attr['RFC822.SIZE'],
181
+ Time.parse(data.attr['INTERNALDATE']).to_i))
182
+ end
183
+ end
184
+
185
+ # Examines the mailbox. If _force_ is true, the mailbox will be examined even
186
+ # if it is already selected (which isn't necessary unless you want to ensure
187
+ # that it's in a read-only state).
188
+ def imap_examine(force = false)
189
+ return if @state == :examined || (!force && @state == :selected)
190
+
191
+ @imap.safely do
192
+ begin
193
+ @mutex.synchronize { @state = :closed }
194
+
195
+ debug "examining mailbox"
196
+ @imap.conn.examine(@name)
197
+
198
+ @mutex.synchronize { @state = :examined }
199
+
200
+ rescue Net::IMAP::NoResponseError => e
201
+ raise Error, "unable to examine mailbox: #{e.message}"
202
+ end
203
+ end
204
+ end
205
+
206
+ # Fetches the specified _fields_ for the specified message sequence id(s) from
207
+ # this mailbox.
208
+ def imap_fetch(ids, fields)
209
+ ids = ids.to_a
210
+ data = []
211
+ pos = 0
212
+
213
+ while pos < ids.length
214
+ @imap.safely do
215
+ imap_examine
216
+
217
+ data += @imap.conn.fetch(ids[pos, MAX_FETCH_COUNT], fields)
218
+ pos += MAX_FETCH_COUNT
219
+ end
220
+ end
221
+
222
+ data
223
+ end
224
+
225
+ # Selects the mailbox if it is not already selected. If the mailbox does not
226
+ # exist and _create_ is +true+, it will be created. Otherwise, a
227
+ # Larch::IMAP::Error will be raised.
228
+ def imap_select(create = false)
229
+ return if @state == :selected
230
+
231
+ @imap.safely do
232
+ begin
233
+ @mutex.synchronize { @state = :closed }
234
+
235
+ debug "selecting mailbox"
236
+ @imap.conn.select(@name)
237
+
238
+ @mutex.synchronize { @state = :selected }
239
+
240
+ rescue Net::IMAP::NoResponseError => e
241
+ raise Error, "unable to select mailbox: #{e.message}" unless create
242
+
243
+ info "creating mailbox: #{mailbox}"
244
+
245
+ begin
246
+ @imap.conn.create(@name)
247
+ retry
248
+ rescue => e
249
+ raise Error, "unable to create mailbox: #{e.message}"
250
+ end
251
+ end
252
+ end
253
+ end
254
+
255
+ # Fetches the specified _fields_ for the specified UID(s) from the IMAP
256
+ # server.
257
+ def imap_uid_fetch(uids, fields)
258
+ uids = uids.to_a
259
+ data = []
260
+ pos = 0
261
+
262
+ while pos < uids.length
263
+ @imap.safely do
264
+ imap_examine
265
+
266
+ data += @imap.conn.uid_fetch(uids[pos, MAX_FETCH_COUNT], fields)
267
+ pos += MAX_FETCH_COUNT
268
+ end
269
+ end
270
+
271
+ data
272
+ end
273
+
274
+ end
275
+
276
+ end; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rgrove-larch
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.8
4
+ version: 1.0.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Grove
@@ -48,6 +48,7 @@ files:
48
48
  - lib/larch.rb
49
49
  - lib/larch/errors.rb
50
50
  - lib/larch/imap.rb
51
+ - lib/larch/imap/mailbox.rb
51
52
  - lib/larch/logger.rb
52
53
  - lib/larch/version.rb
53
54
  has_rdoc: false