tmail 1.1.1
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.
- data/LICENSE +21 -0
- data/README +157 -0
- data/bat/changelog +19 -0
- data/bat/clobber/package +10 -0
- data/bat/compile +42 -0
- data/bat/config.yaml +8 -0
- data/bat/prepare +8 -0
- data/bat/publish +51 -0
- data/bat/rdoc +42 -0
- data/bat/release +12 -0
- data/bat/setup +1616 -0
- data/bat/stats +138 -0
- data/bat/tag +25 -0
- data/bat/test +25 -0
- data/ext/tmail/Makefile +25 -0
- data/ext/tmail/base64/MANIFEST +4 -0
- data/ext/tmail/base64/base64.c +264 -0
- data/ext/tmail/base64/depend +1 -0
- data/ext/tmail/base64/extconf.rb +38 -0
- data/ext/tmail/scanner_c/MANIFEST +4 -0
- data/ext/tmail/scanner_c/depend +1 -0
- data/ext/tmail/scanner_c/extconf.rb +38 -0
- data/ext/tmail/scanner_c/scanner_c.c +582 -0
- data/lib/tmail.rb +4 -0
- data/lib/tmail/Makefile +19 -0
- data/lib/tmail/address.rb +245 -0
- data/lib/tmail/attachments.rb +47 -0
- data/lib/tmail/base64.rb +75 -0
- data/lib/tmail/compat.rb +39 -0
- data/lib/tmail/config.rb +71 -0
- data/lib/tmail/core_extensions.rb +67 -0
- data/lib/tmail/encode.rb +524 -0
- data/lib/tmail/header.rb +931 -0
- data/lib/tmail/index.rb +8 -0
- data/lib/tmail/interface.rb +540 -0
- data/lib/tmail/loader.rb +1 -0
- data/lib/tmail/mail.rb +507 -0
- data/lib/tmail/mailbox.rb +435 -0
- data/lib/tmail/mbox.rb +1 -0
- data/lib/tmail/net.rb +282 -0
- data/lib/tmail/obsolete.rb +137 -0
- data/lib/tmail/parser.rb +1475 -0
- data/lib/tmail/parser.y +381 -0
- data/lib/tmail/port.rb +379 -0
- data/lib/tmail/quoting.rb +142 -0
- data/lib/tmail/require_arch.rb +56 -0
- data/lib/tmail/scanner.rb +44 -0
- data/lib/tmail/scanner_r.rb +263 -0
- data/lib/tmail/stringio.rb +279 -0
- data/lib/tmail/tmail.rb +1 -0
- data/lib/tmail/utils.rb +281 -0
- data/lib/tmail/version.rb +38 -0
- data/meta/icli.yaml +16 -0
- data/meta/tmail-1.1.1.roll +24 -0
- data/sample/data/multipart +23 -0
- data/sample/data/normal +29 -0
- data/sample/data/sendtest +5 -0
- data/sample/data/simple +14 -0
- data/sample/data/test +27 -0
- data/sample/extract-attachements.rb +33 -0
- data/sample/from-check.rb +26 -0
- data/sample/multipart.rb +26 -0
- data/sample/parse-bench.rb +68 -0
- data/sample/parse-test.rb +19 -0
- data/sample/sendmail.rb +94 -0
- data/test/extctrl.rb +6 -0
- data/test/fixtures/raw_base64_decoded_string +0 -0
- data/test/fixtures/raw_base64_email +83 -0
- data/test/fixtures/raw_base64_encoded_string +1 -0
- data/test/fixtures/raw_email +14 -0
- data/test/fixtures/raw_email10 +20 -0
- data/test/fixtures/raw_email11 +34 -0
- data/test/fixtures/raw_email12 +32 -0
- data/test/fixtures/raw_email13 +29 -0
- data/test/fixtures/raw_email2 +114 -0
- data/test/fixtures/raw_email3 +70 -0
- data/test/fixtures/raw_email4 +59 -0
- data/test/fixtures/raw_email5 +19 -0
- data/test/fixtures/raw_email6 +20 -0
- data/test/fixtures/raw_email7 +66 -0
- data/test/fixtures/raw_email8 +47 -0
- data/test/fixtures/raw_email9 +28 -0
- data/test/fixtures/raw_email_quoted_with_0d0a +14 -0
- data/test/fixtures/raw_email_simple +11 -0
- data/test/fixtures/raw_email_with_illegal_boundary +58 -0
- data/test/fixtures/raw_email_with_multipart_mixed_quoted_boundary +50 -0
- data/test/fixtures/raw_email_with_nested_attachment +100 -0
- data/test/fixtures/raw_email_with_partially_quoted_subject +14 -0
- data/test/fixtures/raw_email_with_quoted_illegal_boundary +58 -0
- data/test/kcode.rb +14 -0
- data/test/test_address.rb +1128 -0
- data/test/test_attachments.rb +35 -0
- data/test/test_base64.rb +63 -0
- data/test/test_encode.rb +77 -0
- data/test/test_header.rb +885 -0
- data/test/test_helper.rb +2 -0
- data/test/test_mail.rb +623 -0
- data/test/test_mbox.rb +126 -0
- data/test/test_port.rb +430 -0
- data/test/test_scanner.rb +209 -0
- data/test/test_utils.rb +37 -0
- metadata +205 -0
data/lib/tmail/parser.y
ADDED
@@ -0,0 +1,381 @@
|
|
1
|
+
#
|
2
|
+
# parser.y
|
3
|
+
#
|
4
|
+
# Copyright (c) 1998-2007 Minero Aoki
|
5
|
+
#
|
6
|
+
# This program is free software.
|
7
|
+
# You can distribute/modify this program under the terms of
|
8
|
+
# the GNU Lesser General Public License version 2.1.
|
9
|
+
#
|
10
|
+
|
11
|
+
class TMail::Parser
|
12
|
+
|
13
|
+
options no_result_var
|
14
|
+
|
15
|
+
rule
|
16
|
+
|
17
|
+
content : DATETIME datetime { val[1] }
|
18
|
+
| RECEIVED received { val[1] }
|
19
|
+
| MADDRESS addrs_TOP { val[1] }
|
20
|
+
| RETPATH retpath { val[1] }
|
21
|
+
| KEYWORDS keys { val[1] }
|
22
|
+
| ENCRYPTED enc { val[1] }
|
23
|
+
| MIMEVERSION version { val[1] }
|
24
|
+
| CTYPE ctype { val[1] }
|
25
|
+
| CENCODING cencode { val[1] }
|
26
|
+
| CDISPOSITION cdisp { val[1] }
|
27
|
+
| ADDRESS addr_TOP { val[1] }
|
28
|
+
| MAILBOX mbox { val[1] }
|
29
|
+
|
30
|
+
datetime : day DIGIT ATOM DIGIT hour zone
|
31
|
+
# 0 1 2 3 4 5
|
32
|
+
# date month year
|
33
|
+
{
|
34
|
+
t = Time.gm(val[3].to_i, val[2], val[1].to_i, 0, 0, 0)
|
35
|
+
(t + val[4] - val[5]).localtime
|
36
|
+
}
|
37
|
+
|
38
|
+
day : /* none */
|
39
|
+
| ATOM ','
|
40
|
+
|
41
|
+
hour : DIGIT ':' DIGIT
|
42
|
+
{
|
43
|
+
(val[0].to_i * 60 * 60) +
|
44
|
+
(val[2].to_i * 60)
|
45
|
+
}
|
46
|
+
| DIGIT ':' DIGIT ':' DIGIT
|
47
|
+
{
|
48
|
+
(val[0].to_i * 60 * 60) +
|
49
|
+
(val[2].to_i * 60) +
|
50
|
+
(val[4].to_i)
|
51
|
+
}
|
52
|
+
|
53
|
+
zone : ATOM
|
54
|
+
{
|
55
|
+
timezone_string_to_unixtime(val[0])
|
56
|
+
}
|
57
|
+
|
58
|
+
received : from by via with id for received_datetime
|
59
|
+
{
|
60
|
+
val
|
61
|
+
}
|
62
|
+
|
63
|
+
from : /* none */
|
64
|
+
| FROM received_domain
|
65
|
+
{
|
66
|
+
val[1]
|
67
|
+
}
|
68
|
+
|
69
|
+
by : /* none */
|
70
|
+
| BY received_domain
|
71
|
+
{
|
72
|
+
val[1]
|
73
|
+
}
|
74
|
+
|
75
|
+
received_domain
|
76
|
+
: domain
|
77
|
+
{
|
78
|
+
join_domain(val[0])
|
79
|
+
}
|
80
|
+
| domain '@' domain
|
81
|
+
{
|
82
|
+
join_domain(val[2])
|
83
|
+
}
|
84
|
+
| domain DOMLIT
|
85
|
+
{
|
86
|
+
join_domain(val[0])
|
87
|
+
}
|
88
|
+
|
89
|
+
via : /* none */
|
90
|
+
| VIA ATOM
|
91
|
+
{
|
92
|
+
val[1]
|
93
|
+
}
|
94
|
+
|
95
|
+
with : /* none */
|
96
|
+
{
|
97
|
+
[]
|
98
|
+
}
|
99
|
+
| with WITH ATOM
|
100
|
+
{
|
101
|
+
val[0].push val[2]
|
102
|
+
val[0]
|
103
|
+
}
|
104
|
+
|
105
|
+
id : /* none */
|
106
|
+
| ID msgid
|
107
|
+
{
|
108
|
+
val[1]
|
109
|
+
}
|
110
|
+
| ID ATOM
|
111
|
+
{
|
112
|
+
val[1]
|
113
|
+
}
|
114
|
+
|
115
|
+
for : /* none */
|
116
|
+
| FOR received_addrspec
|
117
|
+
{
|
118
|
+
val[1]
|
119
|
+
}
|
120
|
+
|
121
|
+
received_addrspec
|
122
|
+
: routeaddr
|
123
|
+
{
|
124
|
+
val[0].spec
|
125
|
+
}
|
126
|
+
| spec
|
127
|
+
{
|
128
|
+
val[0].spec
|
129
|
+
}
|
130
|
+
|
131
|
+
received_datetime
|
132
|
+
: /* none */
|
133
|
+
| ';' datetime
|
134
|
+
{
|
135
|
+
val[1]
|
136
|
+
}
|
137
|
+
|
138
|
+
addrs_TOP : addrs
|
139
|
+
| group_bare
|
140
|
+
| addrs commas group_bare
|
141
|
+
|
142
|
+
addr_TOP : mbox
|
143
|
+
| group
|
144
|
+
| group_bare
|
145
|
+
|
146
|
+
retpath : addrs_TOP
|
147
|
+
| '<' '>' { [ Address.new(nil, nil) ] }
|
148
|
+
|
149
|
+
addrs : addr
|
150
|
+
{
|
151
|
+
val
|
152
|
+
}
|
153
|
+
| addrs commas addr
|
154
|
+
{
|
155
|
+
val[0].push val[2]
|
156
|
+
val[0]
|
157
|
+
}
|
158
|
+
|
159
|
+
addr : mbox
|
160
|
+
| group
|
161
|
+
|
162
|
+
mboxes : mbox
|
163
|
+
{
|
164
|
+
val
|
165
|
+
}
|
166
|
+
| mboxes commas mbox
|
167
|
+
{
|
168
|
+
val[0].push val[2]
|
169
|
+
val[0]
|
170
|
+
}
|
171
|
+
|
172
|
+
mbox : spec
|
173
|
+
| routeaddr
|
174
|
+
| addr_phrase routeaddr
|
175
|
+
{
|
176
|
+
val[1].phrase = Decoder.decode(val[0])
|
177
|
+
val[1]
|
178
|
+
}
|
179
|
+
|
180
|
+
group : group_bare ';'
|
181
|
+
|
182
|
+
group_bare: addr_phrase ':' mboxes
|
183
|
+
{
|
184
|
+
AddressGroup.new(val[0], val[2])
|
185
|
+
}
|
186
|
+
| addr_phrase ':' { AddressGroup.new(val[0], []) }
|
187
|
+
|
188
|
+
addr_phrase
|
189
|
+
: local_head { val[0].join('.') }
|
190
|
+
| addr_phrase local_head { val[0] << ' ' << val[1].join('.') }
|
191
|
+
|
192
|
+
routeaddr : '<' routes spec '>'
|
193
|
+
{
|
194
|
+
val[2].routes.replace val[1]
|
195
|
+
val[2]
|
196
|
+
}
|
197
|
+
| '<' spec '>'
|
198
|
+
{
|
199
|
+
val[1]
|
200
|
+
}
|
201
|
+
|
202
|
+
routes : at_domains ':'
|
203
|
+
|
204
|
+
at_domains: '@' domain { [ val[1].join('.') ] }
|
205
|
+
| at_domains ',' '@' domain { val[0].push val[3].join('.'); val[0] }
|
206
|
+
|
207
|
+
spec : local '@' domain { Address.new( val[0], val[2] ) }
|
208
|
+
| local { Address.new( val[0], nil ) }
|
209
|
+
|
210
|
+
local: local_head
|
211
|
+
| local_head '.' { val[0].push ''; val[0] }
|
212
|
+
|
213
|
+
local_head: word
|
214
|
+
{ val }
|
215
|
+
| local_head dots word
|
216
|
+
{
|
217
|
+
val[1].times do
|
218
|
+
val[0].push ''
|
219
|
+
end
|
220
|
+
val[0].push val[2]
|
221
|
+
val[0]
|
222
|
+
}
|
223
|
+
|
224
|
+
domain : domword
|
225
|
+
{ val }
|
226
|
+
| domain dots domword
|
227
|
+
{
|
228
|
+
val[1].times do
|
229
|
+
val[0].push ''
|
230
|
+
end
|
231
|
+
val[0].push val[2]
|
232
|
+
val[0]
|
233
|
+
}
|
234
|
+
|
235
|
+
dots : '.' { 0 }
|
236
|
+
| '.' '.' { 1 }
|
237
|
+
|
238
|
+
word : atom
|
239
|
+
| QUOTED
|
240
|
+
| DIGIT
|
241
|
+
|
242
|
+
domword : atom
|
243
|
+
| DOMLIT
|
244
|
+
| DIGIT
|
245
|
+
|
246
|
+
commas : ','
|
247
|
+
| commas ','
|
248
|
+
|
249
|
+
msgid : '<' spec '>'
|
250
|
+
{
|
251
|
+
val[1] = val[1].spec
|
252
|
+
val.join('')
|
253
|
+
}
|
254
|
+
|
255
|
+
keys : phrase { val }
|
256
|
+
| keys ',' phrase { val[0].push val[2]; val[0] }
|
257
|
+
|
258
|
+
phrase : word
|
259
|
+
| phrase word { val[0] << ' ' << val[1] }
|
260
|
+
|
261
|
+
enc : word
|
262
|
+
{
|
263
|
+
val.push nil
|
264
|
+
val
|
265
|
+
}
|
266
|
+
| word word
|
267
|
+
{
|
268
|
+
val
|
269
|
+
}
|
270
|
+
|
271
|
+
version : DIGIT '.' DIGIT
|
272
|
+
{
|
273
|
+
[ val[0].to_i, val[2].to_i ]
|
274
|
+
}
|
275
|
+
|
276
|
+
ctype : TOKEN '/' TOKEN params opt_semicolon
|
277
|
+
{
|
278
|
+
[ val[0].downcase, val[2].downcase, decode_params(val[3]) ]
|
279
|
+
}
|
280
|
+
| TOKEN params opt_semicolon
|
281
|
+
{
|
282
|
+
[ val[0].downcase, nil, decode_params(val[1]) ]
|
283
|
+
}
|
284
|
+
|
285
|
+
params : /* none */
|
286
|
+
{
|
287
|
+
{}
|
288
|
+
}
|
289
|
+
| params ';' TOKEN '=' QUOTED
|
290
|
+
{
|
291
|
+
val[0][ val[2].downcase ] = ('"' + val[4].to_s + '"')
|
292
|
+
val[0]
|
293
|
+
}
|
294
|
+
| params ';' TOKEN '=' TOKEN
|
295
|
+
{
|
296
|
+
val[0][ val[2].downcase ] = val[4]
|
297
|
+
val[0]
|
298
|
+
}
|
299
|
+
|
300
|
+
cencode : TOKEN
|
301
|
+
{
|
302
|
+
val[0].downcase
|
303
|
+
}
|
304
|
+
|
305
|
+
cdisp : TOKEN params opt_semicolon
|
306
|
+
{
|
307
|
+
[ val[0].downcase, decode_params(val[1]) ]
|
308
|
+
}
|
309
|
+
|
310
|
+
opt_semicolon
|
311
|
+
:
|
312
|
+
| ';'
|
313
|
+
|
314
|
+
atom : ATOM
|
315
|
+
| FROM
|
316
|
+
| BY
|
317
|
+
| VIA
|
318
|
+
| WITH
|
319
|
+
| ID
|
320
|
+
| FOR
|
321
|
+
|
322
|
+
end
|
323
|
+
|
324
|
+
|
325
|
+
---- header
|
326
|
+
#
|
327
|
+
# parser.rb
|
328
|
+
#
|
329
|
+
# Copyright (c) 1998-2007 Minero Aoki
|
330
|
+
#
|
331
|
+
# This program is free software.
|
332
|
+
# You can distribute/modify this program under the terms of
|
333
|
+
# the GNU Lesser General Public License version 2.1.
|
334
|
+
#
|
335
|
+
|
336
|
+
require 'tmail/scanner'
|
337
|
+
require 'tmail/utils'
|
338
|
+
|
339
|
+
---- inner
|
340
|
+
|
341
|
+
include TextUtils
|
342
|
+
|
343
|
+
def self.parse( ident, str, cmt = nil )
|
344
|
+
new.parse(ident, str, cmt)
|
345
|
+
end
|
346
|
+
|
347
|
+
MAILP_DEBUG = false
|
348
|
+
|
349
|
+
def initialize
|
350
|
+
self.debug = MAILP_DEBUG
|
351
|
+
end
|
352
|
+
|
353
|
+
def debug=( flag )
|
354
|
+
@yydebug = flag && Racc_debug_parser
|
355
|
+
@scanner_debug = flag
|
356
|
+
end
|
357
|
+
|
358
|
+
def debug
|
359
|
+
@yydebug
|
360
|
+
end
|
361
|
+
|
362
|
+
def parse( ident, str, comments = nil )
|
363
|
+
@scanner = Scanner.new(str, ident, comments)
|
364
|
+
@scanner.debug = @scanner_debug
|
365
|
+
@first = [ident, ident]
|
366
|
+
result = yyparse(self, :parse_in)
|
367
|
+
comments.map! {|c| to_kcode(c) } if comments
|
368
|
+
result
|
369
|
+
end
|
370
|
+
|
371
|
+
private
|
372
|
+
|
373
|
+
def parse_in( &block )
|
374
|
+
yield @first
|
375
|
+
@scanner.scan(&block)
|
376
|
+
end
|
377
|
+
|
378
|
+
def on_error( t, val, vstack )
|
379
|
+
raise SyntaxError, "parse error on token #{racc_token2str t}"
|
380
|
+
end
|
381
|
+
|
data/lib/tmail/port.rb
ADDED
@@ -0,0 +1,379 @@
|
|
1
|
+
=begin rdoc
|
2
|
+
|
3
|
+
= Port class
|
4
|
+
|
5
|
+
=end
|
6
|
+
#--
|
7
|
+
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
#
|
28
|
+
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
|
29
|
+
# with permission of Minero Aoki.
|
30
|
+
#++
|
31
|
+
|
32
|
+
require 'tmail/stringio'
|
33
|
+
|
34
|
+
|
35
|
+
module TMail
|
36
|
+
|
37
|
+
class Port
|
38
|
+
def reproducible?
|
39
|
+
false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
###
|
45
|
+
### FilePort
|
46
|
+
###
|
47
|
+
|
48
|
+
class FilePort < Port
|
49
|
+
|
50
|
+
def initialize( fname )
|
51
|
+
@filename = File.expand_path(fname)
|
52
|
+
super()
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :filename
|
56
|
+
|
57
|
+
alias ident filename
|
58
|
+
|
59
|
+
def ==( other )
|
60
|
+
other.respond_to?(:filename) and @filename == other.filename
|
61
|
+
end
|
62
|
+
|
63
|
+
alias eql? ==
|
64
|
+
|
65
|
+
def hash
|
66
|
+
@filename.hash
|
67
|
+
end
|
68
|
+
|
69
|
+
def inspect
|
70
|
+
"#<#{self.class}:#{@filename}>"
|
71
|
+
end
|
72
|
+
|
73
|
+
def reproducible?
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
def size
|
78
|
+
File.size @filename
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def ropen( &block )
|
83
|
+
File.open(@filename, &block)
|
84
|
+
end
|
85
|
+
|
86
|
+
def wopen( &block )
|
87
|
+
File.open(@filename, 'w', &block)
|
88
|
+
end
|
89
|
+
|
90
|
+
def aopen( &block )
|
91
|
+
File.open(@filename, 'a', &block)
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def read_all
|
96
|
+
ropen {|f|
|
97
|
+
return f.read
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
def remove
|
103
|
+
File.unlink @filename
|
104
|
+
end
|
105
|
+
|
106
|
+
def move_to( port )
|
107
|
+
begin
|
108
|
+
File.link @filename, port.filename
|
109
|
+
rescue Errno::EXDEV
|
110
|
+
copy_to port
|
111
|
+
end
|
112
|
+
File.unlink @filename
|
113
|
+
end
|
114
|
+
|
115
|
+
alias mv move_to
|
116
|
+
|
117
|
+
def copy_to( port )
|
118
|
+
if FilePort === port
|
119
|
+
copy_file @filename, port.filename
|
120
|
+
else
|
121
|
+
File.open(@filename) {|r|
|
122
|
+
port.wopen {|w|
|
123
|
+
while s = r.sysread(4096)
|
124
|
+
w.write << s
|
125
|
+
end
|
126
|
+
} }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
alias cp copy_to
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
# from fileutils.rb
|
135
|
+
def copy_file( src, dest )
|
136
|
+
st = r = w = nil
|
137
|
+
|
138
|
+
File.open(src, 'rb') {|r|
|
139
|
+
File.open(dest, 'wb') {|w|
|
140
|
+
st = r.stat
|
141
|
+
begin
|
142
|
+
while true
|
143
|
+
w.write r.sysread(st.blksize)
|
144
|
+
end
|
145
|
+
rescue EOFError
|
146
|
+
end
|
147
|
+
} }
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
module MailFlags
|
154
|
+
|
155
|
+
def seen=( b )
|
156
|
+
set_status 'S', b
|
157
|
+
end
|
158
|
+
|
159
|
+
def seen?
|
160
|
+
get_status 'S'
|
161
|
+
end
|
162
|
+
|
163
|
+
def replied=( b )
|
164
|
+
set_status 'R', b
|
165
|
+
end
|
166
|
+
|
167
|
+
def replied?
|
168
|
+
get_status 'R'
|
169
|
+
end
|
170
|
+
|
171
|
+
def flagged=( b )
|
172
|
+
set_status 'F', b
|
173
|
+
end
|
174
|
+
|
175
|
+
def flagged?
|
176
|
+
get_status 'F'
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def procinfostr( str, tag, true_p )
|
182
|
+
a = str.upcase.split(//)
|
183
|
+
a.push true_p ? tag : nil
|
184
|
+
a.delete tag unless true_p
|
185
|
+
a.compact.sort.join('').squeeze
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
class MhPort < FilePort
|
192
|
+
|
193
|
+
include MailFlags
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
def set_status( tag, flag )
|
198
|
+
begin
|
199
|
+
tmpfile = @filename + '.tmailtmp.' + $$.to_s
|
200
|
+
File.open(tmpfile, 'w') {|f|
|
201
|
+
write_status f, tag, flag
|
202
|
+
}
|
203
|
+
File.unlink @filename
|
204
|
+
File.link tmpfile, @filename
|
205
|
+
ensure
|
206
|
+
File.unlink tmpfile
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def write_status( f, tag, flag )
|
211
|
+
stat = ''
|
212
|
+
File.open(@filename) {|r|
|
213
|
+
while line = r.gets
|
214
|
+
if line.strip.empty?
|
215
|
+
break
|
216
|
+
elsif m = /\AX-TMail-Status:/i.match(line)
|
217
|
+
stat = m.post_match.strip
|
218
|
+
else
|
219
|
+
f.print line
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
s = procinfostr(stat, tag, flag)
|
224
|
+
f.puts 'X-TMail-Status: ' + s unless s.empty?
|
225
|
+
f.puts
|
226
|
+
|
227
|
+
while s = r.read(2048)
|
228
|
+
f.write s
|
229
|
+
end
|
230
|
+
}
|
231
|
+
end
|
232
|
+
|
233
|
+
def get_status( tag )
|
234
|
+
File.foreach(@filename) {|line|
|
235
|
+
return false if line.strip.empty?
|
236
|
+
if m = /\AX-TMail-Status:/i.match(line)
|
237
|
+
return m.post_match.strip.include?(tag[0])
|
238
|
+
end
|
239
|
+
}
|
240
|
+
false
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
|
246
|
+
class MaildirPort < FilePort
|
247
|
+
|
248
|
+
def move_to_new
|
249
|
+
new = replace_dir(@filename, 'new')
|
250
|
+
File.rename @filename, new
|
251
|
+
@filename = new
|
252
|
+
end
|
253
|
+
|
254
|
+
def move_to_cur
|
255
|
+
new = replace_dir(@filename, 'cur')
|
256
|
+
File.rename @filename, new
|
257
|
+
@filename = new
|
258
|
+
end
|
259
|
+
|
260
|
+
def replace_dir( path, dir )
|
261
|
+
"#{File.dirname File.dirname(path)}/#{dir}/#{File.basename path}"
|
262
|
+
end
|
263
|
+
private :replace_dir
|
264
|
+
|
265
|
+
|
266
|
+
include MailFlags
|
267
|
+
|
268
|
+
private
|
269
|
+
|
270
|
+
MAIL_FILE = /\A(\d+\.[\d_]+\.[^:]+)(?:\:(\d),(\w+)?)?\z/
|
271
|
+
|
272
|
+
def set_status( tag, flag )
|
273
|
+
if m = MAIL_FILE.match(File.basename(@filename))
|
274
|
+
s, uniq, type, info, = m.to_a
|
275
|
+
return if type and type != '2' # do not change anything
|
276
|
+
newname = File.dirname(@filename) + '/' +
|
277
|
+
uniq + ':2,' + procinfostr(info.to_s, tag, flag)
|
278
|
+
else
|
279
|
+
newname = @filename + ':2,' + tag
|
280
|
+
end
|
281
|
+
|
282
|
+
File.link @filename, newname
|
283
|
+
File.unlink @filename
|
284
|
+
@filename = newname
|
285
|
+
end
|
286
|
+
|
287
|
+
def get_status( tag )
|
288
|
+
m = MAIL_FILE.match(File.basename(@filename)) or return false
|
289
|
+
m[2] == '2' and m[3].to_s.include?(tag[0])
|
290
|
+
end
|
291
|
+
|
292
|
+
end
|
293
|
+
|
294
|
+
|
295
|
+
###
|
296
|
+
### StringPort
|
297
|
+
###
|
298
|
+
|
299
|
+
class StringPort < Port
|
300
|
+
|
301
|
+
def initialize( str = '' )
|
302
|
+
@buffer = str
|
303
|
+
super()
|
304
|
+
end
|
305
|
+
|
306
|
+
def string
|
307
|
+
@buffer
|
308
|
+
end
|
309
|
+
|
310
|
+
def to_s
|
311
|
+
@buffer.dup
|
312
|
+
end
|
313
|
+
|
314
|
+
alias read_all to_s
|
315
|
+
|
316
|
+
def size
|
317
|
+
@buffer.size
|
318
|
+
end
|
319
|
+
|
320
|
+
def ==( other )
|
321
|
+
StringPort === other and @buffer.equal? other.string
|
322
|
+
end
|
323
|
+
|
324
|
+
alias eql? ==
|
325
|
+
|
326
|
+
def hash
|
327
|
+
@buffer.object_id.hash
|
328
|
+
end
|
329
|
+
|
330
|
+
def inspect
|
331
|
+
"#<#{self.class}:id=#{sprintf '0x%x', @buffer.object_id}>"
|
332
|
+
end
|
333
|
+
|
334
|
+
def reproducible?
|
335
|
+
true
|
336
|
+
end
|
337
|
+
|
338
|
+
def ropen( &block )
|
339
|
+
@buffer or raise Errno::ENOENT, "#{inspect} is already removed"
|
340
|
+
StringInput.open(@buffer, &block)
|
341
|
+
end
|
342
|
+
|
343
|
+
def wopen( &block )
|
344
|
+
@buffer = ''
|
345
|
+
StringOutput.new(@buffer, &block)
|
346
|
+
end
|
347
|
+
|
348
|
+
def aopen( &block )
|
349
|
+
@buffer ||= ''
|
350
|
+
StringOutput.new(@buffer, &block)
|
351
|
+
end
|
352
|
+
|
353
|
+
def remove
|
354
|
+
@buffer = nil
|
355
|
+
end
|
356
|
+
|
357
|
+
alias rm remove
|
358
|
+
|
359
|
+
def copy_to( port )
|
360
|
+
port.wopen {|f|
|
361
|
+
f.write @buffer
|
362
|
+
}
|
363
|
+
end
|
364
|
+
|
365
|
+
alias cp copy_to
|
366
|
+
|
367
|
+
def move_to( port )
|
368
|
+
if StringPort === port
|
369
|
+
str = @buffer
|
370
|
+
port.instance_eval { @buffer = str }
|
371
|
+
else
|
372
|
+
copy_to port
|
373
|
+
end
|
374
|
+
remove
|
375
|
+
end
|
376
|
+
|
377
|
+
end
|
378
|
+
|
379
|
+
end # module TMail
|