rims 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/ChangeLog +379 -0
  4. data/Gemfile +11 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +566 -0
  7. data/Rakefile +29 -0
  8. data/bin/rims +11 -0
  9. data/lib/rims.rb +45 -0
  10. data/lib/rims/auth.rb +133 -0
  11. data/lib/rims/cksum_kvs.rb +68 -0
  12. data/lib/rims/cmd.rb +809 -0
  13. data/lib/rims/daemon.rb +338 -0
  14. data/lib/rims/db.rb +793 -0
  15. data/lib/rims/error.rb +23 -0
  16. data/lib/rims/gdbm_kvs.rb +76 -0
  17. data/lib/rims/hash_kvs.rb +66 -0
  18. data/lib/rims/kvs.rb +101 -0
  19. data/lib/rims/lock.rb +151 -0
  20. data/lib/rims/mail_store.rb +663 -0
  21. data/lib/rims/passwd.rb +251 -0
  22. data/lib/rims/pool.rb +88 -0
  23. data/lib/rims/protocol.rb +71 -0
  24. data/lib/rims/protocol/decoder.rb +1469 -0
  25. data/lib/rims/protocol/parser.rb +1114 -0
  26. data/lib/rims/rfc822.rb +456 -0
  27. data/lib/rims/server.rb +567 -0
  28. data/lib/rims/test.rb +391 -0
  29. data/lib/rims/version.rb +11 -0
  30. data/load_test/Rakefile +93 -0
  31. data/rims.gemspec +38 -0
  32. data/test/test_auth.rb +174 -0
  33. data/test/test_cksum_kvs.rb +121 -0
  34. data/test/test_config.rb +533 -0
  35. data/test/test_daemon_status_file.rb +169 -0
  36. data/test/test_daemon_waitpid.rb +72 -0
  37. data/test/test_db.rb +602 -0
  38. data/test/test_db_recovery.rb +732 -0
  39. data/test/test_error.rb +97 -0
  40. data/test/test_gdbm_kvs.rb +32 -0
  41. data/test/test_hash_kvs.rb +116 -0
  42. data/test/test_lock.rb +161 -0
  43. data/test/test_mail_store.rb +750 -0
  44. data/test/test_passwd.rb +203 -0
  45. data/test/test_protocol.rb +91 -0
  46. data/test/test_protocol_auth.rb +121 -0
  47. data/test/test_protocol_decoder.rb +6490 -0
  48. data/test/test_protocol_fetch.rb +994 -0
  49. data/test/test_protocol_request.rb +332 -0
  50. data/test/test_protocol_search.rb +974 -0
  51. data/test/test_rfc822.rb +696 -0
  52. metadata +174 -0
@@ -0,0 +1,1114 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'set'
4
+ require 'time'
5
+
6
+ module RIMS
7
+ module Protocol
8
+ FetchBody = Struct.new(:symbol, :option, :section, :section_list, :partial_origin, :partial_size)
9
+
10
+ class FetchBody
11
+ def fetch_att_name
12
+ s = ''
13
+ s << symbol
14
+ s << '.' << option if option
15
+ s << '[' << section << ']'
16
+ if (partial_origin) then
17
+ s << '<' << partial_origin.to_s << '.' << partial_size.to_s << '>'
18
+ end
19
+
20
+ s
21
+ end
22
+
23
+ def msg_att_name
24
+ s = ''
25
+ s << symbol
26
+ s << '[' << section << ']'
27
+ if (partial_origin) then
28
+ s << '<' << partial_origin.to_s << '>'
29
+ end
30
+
31
+ s
32
+ end
33
+ end
34
+
35
+ class RequestReader
36
+ def initialize(input, output, logger)
37
+ @input = input
38
+ @output = output
39
+ @logger = logger
40
+ end
41
+
42
+ def read_line
43
+ line = @input.gets or return
44
+ @logger.debug("read line: #{Protocol.io_data_log(line)}") if @logger.debug?
45
+ line.chomp!("\n")
46
+ line.chomp!("\r")
47
+ scan_line(line)
48
+ end
49
+
50
+ def scan_line(line)
51
+ atom_list = line.scan(/BODY(?:\.\S+)?\[.*?\](?:<\d+\.\d+>)?|[\[\]()]|".*?"|[^\[\]()\s]+/i).map{|s|
52
+ case (s)
53
+ when '(', ')', '[', ']', /\ANIL\z/i
54
+ s.upcase.intern
55
+ when /\A"/
56
+ s.sub(/\A"/, '').sub(/"\z/, '')
57
+ when /\A(?<body_symbol>BODY)(?:\.(?<body_option>\S+))?\[(?<body_section>.*)\](?:<(?<partial_origin>\d+\.(?<partial_size>\d+)>))?\z/i
58
+ body_symbol = $~[:body_symbol]
59
+ body_option = $~[:body_option]
60
+ body_section = $~[:body_section]
61
+ partial_origin = $~[:partial_origin] && $~[:partial_origin].to_i
62
+ partial_size = $~[:partial_size] && $~[:partial_size].to_i
63
+ [ :body,
64
+ Protocol.body(symbol: body_symbol,
65
+ option: body_option,
66
+ section: body_section,
67
+ partial_origin: partial_origin,
68
+ partial_size: partial_size)
69
+ ]
70
+ else
71
+ s
72
+ end
73
+ }
74
+ if ((atom_list[-1].is_a? String) && (atom_list[-1] =~ /\A{\d+}\z/)) then
75
+ next_size = $&[1..-2].to_i
76
+ @logger.debug("found literal: #{next_size} octets.") if @logger.debug?
77
+ @output.write("+ continue\r\n")
78
+ @output.flush
79
+ @logger.debug('continue literal.') if @logger.debug?
80
+ literal_string = @input.read(next_size) or raise 'unexpected client close.'
81
+ @logger.debug("read literal: #{Protocol.io_data_log(literal_string)}") if @logger.debug?
82
+ atom_list[-1] = literal_string
83
+ next_atom_list = read_line or raise 'unexpected client close.'
84
+ atom_list += next_atom_list
85
+ end
86
+
87
+ atom_list
88
+ end
89
+
90
+ def parse(atom_list, last_atom=nil)
91
+ syntax_list = []
92
+ while (atom = atom_list.shift)
93
+ case (atom)
94
+ when last_atom
95
+ break
96
+ when :'('
97
+ syntax_list.push([ :group ] + parse(atom_list, :')'))
98
+ when :'['
99
+ syntax_list.push([ :block ] + parse(atom_list, :']'))
100
+ else
101
+ if ((atom.is_a? Array) && (atom[0] == :body)) then
102
+ body = atom[1]
103
+ body.section_list = parse(scan_line(body.section))
104
+ end
105
+ syntax_list.push(atom)
106
+ end
107
+ end
108
+
109
+ if (atom == nil && last_atom != nil) then
110
+ raise 'syntax error.'
111
+ end
112
+
113
+ syntax_list
114
+ end
115
+
116
+ def read_command
117
+ while (atom_list = read_line)
118
+ if (atom_list.empty?) then
119
+ next
120
+ end
121
+ if (atom_list.length < 2) then
122
+ raise 'need for tag and command.'
123
+ end
124
+ if (atom_list[0] =~ /\A[*+]/) then
125
+ raise "invalid command tag: #{atom_list[0]}"
126
+ end
127
+ return parse(atom_list)
128
+ end
129
+
130
+ nil
131
+ end
132
+ end
133
+
134
+ class AuthenticationReader
135
+ def initialize(auth, input, output, logger)
136
+ @auth = auth
137
+ @input = input
138
+ @output = output
139
+ @logger = logger
140
+ end
141
+
142
+ def authenticate_client(auth_type, inline_client_response_data_base64=nil)
143
+ username = case (auth_type.downcase)
144
+ when 'plain'
145
+ @logger.debug("authentication mechanism: plain") if @logger.debug?
146
+ authenticate_client_plain(inline_client_response_data_base64)
147
+ when 'cram-md5'
148
+ @logger.debug("authentication mechanism: cram-md5") if @logger.debug?
149
+ authenticate_client_cram_md5
150
+ else
151
+ nil
152
+ end
153
+
154
+ case (username)
155
+ when String
156
+ @logger.debug("authenticated #{username}.") if @logger.debug?
157
+ username
158
+ when Symbol
159
+ @logger.debug('no authentication.') if @logger.debug?
160
+ username
161
+ else
162
+ @logger.debug('unauthenticated.') if @logger.debug?
163
+ nil
164
+ end
165
+ end
166
+
167
+ def read_client_response_data(server_challenge_data=nil)
168
+ if (server_challenge_data) then
169
+ server_challenge_data_base64 = Protocol.encode_base64(server_challenge_data)
170
+ @logger.debug("authenticate command: server challenge data: #{Protocol.io_data_log(server_challenge_data_base64)}") if @logger.debug?
171
+ @output.write("+ #{server_challenge_data_base64}\r\n")
172
+ @output.flush
173
+ else
174
+ @logger.debug("authenticate command: server challenge data is nil.") if @logger.debug?
175
+ @output.write("+ \r\n")
176
+ @output.flush
177
+ end
178
+
179
+ if (client_response_data_base64 = @input.gets) then
180
+ client_response_data_base64.strip!
181
+ @logger.debug("authenticate command: client response data: #{Protocol.io_data_log(client_response_data_base64)}") if @logger.debug?
182
+ if (client_response_data_base64 == '*') then
183
+ @logger.debug("authenticate command: no authentication from client.") if @logger.debug?
184
+ return :*
185
+ end
186
+ Protocol.decode_base64(client_response_data_base64)
187
+ end
188
+ end
189
+ private :read_client_response_data
190
+
191
+ def read_client_response_data_plain(inline_client_response_data_base64)
192
+ if (inline_client_response_data_base64) then
193
+ @logger.debug("authenticate command: inline client response data: #{inline_client_response_data_base64}") if @logger.debug?
194
+ Protocol.decode_base64(inline_client_response_data_base64)
195
+ else
196
+ read_client_response_data
197
+ end
198
+ end
199
+ private :read_client_response_data_plain
200
+
201
+ def authenticate_client_plain(inline_client_response_data_base64)
202
+ case (client_response_data = read_client_response_data_plain(inline_client_response_data_base64))
203
+ when String
204
+ @auth.authenticate_plain(client_response_data)
205
+ when Symbol
206
+ client_response_data
207
+ else
208
+ nil
209
+ end
210
+ end
211
+ private :authenticate_client_plain
212
+
213
+ def authenticate_client_cram_md5
214
+ server_challenge_data = @auth.cram_md5_server_challenge_data
215
+ case (client_response_data = read_client_response_data(server_challenge_data))
216
+ when String
217
+ @auth.authenticate_cram_md5(server_challenge_data, client_response_data)
218
+ when Symbol
219
+ client_response_data
220
+ else
221
+ nil
222
+ end
223
+ end
224
+ private :authenticate_client_cram_md5
225
+ end
226
+
227
+ class SearchParser
228
+ def initialize(mail_store, folder)
229
+ @mail_store = mail_store
230
+ @folder = folder
231
+ @charset = nil
232
+ @mail_cache = Hash.new{|hash, uid|
233
+ if (msg_txt = @mail_store.msg_text(@folder.mbox_id, uid)) then
234
+ hash[uid] = RFC822::Message.new(msg_txt)
235
+ end
236
+ }
237
+ end
238
+
239
+ def get_mail(msg)
240
+ @mail_cache[msg.uid] or raise "not found a mail: #{msg.uid}"
241
+ end
242
+ private :get_mail
243
+
244
+ attr_accessor :charset
245
+
246
+ def string_include?(search_string, text)
247
+ if (search_string.ascii_only?) then
248
+ unless (text.encoding.ascii_compatible?) then
249
+ text = text.encode('utf-8')
250
+ end
251
+ else
252
+ if (@charset) then
253
+ search_string = search_string.dup.force_encoding(@charset)
254
+ text = text.encode(@charset)
255
+ end
256
+ end
257
+
258
+ text.include? search_string
259
+ end
260
+ private :string_include?
261
+
262
+ def mail_body_text(mail)
263
+ if (mail.text? || mail.message?) then
264
+ body_txt = mail.body.raw_source
265
+ if (charset = mail.charset) then
266
+ if (body_txt.encoding != Encoding.find(charset)) then
267
+ body_txt = body_txt.dup.force_encoding(charset)
268
+ end
269
+ end
270
+ body_txt
271
+ else
272
+ nil
273
+ end
274
+ end
275
+ private :mail_body_text
276
+
277
+ def end_of_cond
278
+ proc{|msg| true }
279
+ end
280
+ private :end_of_cond
281
+
282
+ def parse_all
283
+ proc{|next_cond|
284
+ proc{|msg|
285
+ next_cond.call(msg)
286
+ }
287
+ }
288
+ end
289
+ private :parse_all
290
+
291
+ def parse_msg_flag_enabled(name)
292
+ proc{|next_cond|
293
+ proc{|msg|
294
+ @mail_store.msg_flag(@folder.mbox_id, msg.uid, name) && next_cond.call(msg)
295
+ }
296
+ }
297
+ end
298
+ private :parse_msg_flag_enabled
299
+
300
+ def parse_msg_flag_disabled(name)
301
+ proc{|next_cond|
302
+ proc{|msg|
303
+ (! @mail_store.msg_flag(@folder.mbox_id, msg.uid, name)) && next_cond.call(msg)
304
+ }
305
+ }
306
+ end
307
+ private :parse_msg_flag_enabled
308
+
309
+ def parse_search_header(field_name, search_string)
310
+ proc{|next_cond|
311
+ proc{|msg|
312
+ mail = get_mail(msg)
313
+ if (mail.header.key? field_name) then
314
+ mail.header.field_value_list(field_name).any?{|field_value|
315
+ string_include?(search_string, field_value)
316
+ } && next_cond.call(msg)
317
+ else
318
+ false
319
+ end
320
+ }
321
+ }
322
+ end
323
+ private :parse_search_header
324
+
325
+ def parse_internal_date(search_time) # :yields: mail_date, boundary
326
+ d = search_time.to_date
327
+ proc{|next_cond|
328
+ proc{|msg|
329
+ yield(@mail_store.msg_date(@folder.mbox_id, msg.uid).to_date, d) && next_cond.call(msg)
330
+ }
331
+ }
332
+ end
333
+ private :parse_internal_date
334
+
335
+ def parse_mail_date(search_time) # :yields: internal_date, boundary
336
+ d = search_time.to_date
337
+ proc{|next_cond|
338
+ proc{|msg|
339
+ if (mail_datetime = get_mail(msg).date) then
340
+ yield(mail_datetime.to_date, d) && next_cond.call(msg)
341
+ else
342
+ false
343
+ end
344
+ }
345
+ }
346
+ end
347
+ private :parse_mail_date
348
+
349
+ def parse_mail_bytesize(octet_size) # :yields: mail_bytesize, boundary
350
+ proc{|next_cond|
351
+ proc{|msg|
352
+ yield(@mail_store.msg_text(@folder.mbox_id, msg.uid).bytesize, octet_size) && next_cond.call(msg)
353
+ }
354
+ }
355
+ end
356
+ private :parse_mail_bytesize
357
+
358
+ def parse_body(search_string)
359
+ proc{|next_cond|
360
+ proc{|msg|
361
+ if (text = mail_body_text(get_mail(msg))) then
362
+ string_include?(search_string, text) && next_cond.call(msg)
363
+ else
364
+ false
365
+ end
366
+ }
367
+ }
368
+ end
369
+ private :parse_body
370
+
371
+ def parse_keyword(search_string)
372
+ proc{|next_cond|
373
+ proc{|msg|
374
+ false
375
+ }
376
+ }
377
+ end
378
+ private :parse_keyword
379
+
380
+ def parse_new
381
+ proc{|next_cond|
382
+ proc{|msg|
383
+ @mail_store.msg_flag(@folder.mbox_id, msg.uid, 'recent') && \
384
+ (! @mail_store.msg_flag(@folder.mbox_id, msg.uid, 'seen')) && next_cond.call(msg)
385
+ }
386
+ }
387
+ end
388
+ private :parse_new
389
+
390
+ def parse_not(next_node)
391
+ operand = next_node.call(end_of_cond)
392
+ proc{|next_cond|
393
+ proc{|msg|
394
+ (! operand.call(msg)) && next_cond.call(msg)
395
+ }
396
+ }
397
+ end
398
+ private :parse_not
399
+
400
+ def parse_old
401
+ proc{|next_cond|
402
+ proc{|msg|
403
+ (! @mail_store.msg_flag(@folder.mbox_id, msg.uid, 'recent')) && next_cond.call(msg)
404
+ }
405
+ }
406
+ end
407
+ private :parse_old
408
+
409
+ def parse_or(next_node1, next_node2)
410
+ operand1 = next_node1.call(end_of_cond)
411
+ operand2 = next_node2.call(end_of_cond)
412
+ proc{|next_cond|
413
+ proc{|msg|
414
+ (operand1.call(msg) || operand2.call(msg)) && next_cond.call(msg)
415
+ }
416
+ }
417
+ end
418
+ private :parse_or
419
+
420
+ def parse_text(search_string)
421
+ search = proc{|text| string_include?(search_string, text) }
422
+ proc{|next_cond|
423
+ proc{|msg|
424
+ mail = get_mail(msg)
425
+ body_txt = mail_body_text(mail)
426
+ (search.call(mail.header.raw_source) || (body_txt && search.call(body_txt))) && next_cond.call(msg)
427
+ }
428
+ }
429
+ end
430
+ private :parse_text
431
+
432
+ def parse_uid(msg_set)
433
+ proc{|next_cond|
434
+ proc{|msg|
435
+ (msg_set.include? msg.uid) && next_cond.call(msg)
436
+ }
437
+ }
438
+ end
439
+ private :parse_uid
440
+
441
+ def parse_unkeyword(search_string)
442
+ parse_all
443
+ end
444
+ private :parse_unkeyword
445
+
446
+ def parse_msg_set(msg_set)
447
+ proc{|next_cond|
448
+ proc{|msg|
449
+ (msg_set.include? msg.num) && next_cond.call(msg)
450
+ }
451
+ }
452
+ end
453
+ private :parse_msg_set
454
+
455
+ def parse_group(search_key)
456
+ group_cond = parse_cached(search_key)
457
+ proc{|next_cond|
458
+ proc{|msg|
459
+ group_cond.call(msg) && next_cond.call(msg)
460
+ }
461
+ }
462
+ end
463
+ private :parse_group
464
+
465
+ def shift_string_value(operation_name, search_key)
466
+ unless (search_string = search_key.shift) then
467
+ raise SyntaxError, "need for a search string of #{operation_name}."
468
+ end
469
+ unless (search_string.is_a? String) then
470
+ raise SyntaxError, "#{operation_name} search string expected as <String> but was <#{search_string.class}>."
471
+ end
472
+
473
+ search_string
474
+ end
475
+ private :shift_string_value
476
+
477
+ def shift_date_value(operation_name, search_key)
478
+ unless (search_date_string = search_key.shift) then
479
+ raise SyntaxError, "need for a search date of #{operation_name}."
480
+ end
481
+ unless (search_date_string.is_a? String) then
482
+ raise SyntaxError, "#{operation_name} search date string expected as <String> but was <#{search_date_string.class}>."
483
+ end
484
+
485
+ begin
486
+ Time.parse(search_date_string)
487
+ rescue ArgumentError
488
+ raise SyntaxError, "#{operation_name} search date is invalid: #{search_date_string}"
489
+ end
490
+ end
491
+ private :shift_date_value
492
+
493
+ def shift_octet_size_value(operation_name, search_key)
494
+ unless (octet_size_string = search_key.shift) then
495
+ raise SyntaxError, "need for a octet size of #{operation_name}."
496
+ end
497
+ unless ((octet_size_string.is_a? String) && (octet_size_string =~ /\A\d+\z/)) then
498
+ raise SyntaxError, "#{operation_name} octet size is expected as numeric string but was <#{octet_size_string}>."
499
+ end
500
+
501
+ octet_size_string.to_i
502
+ end
503
+ private :shift_octet_size_value
504
+
505
+ def fetch_next_node(search_key)
506
+ if (search_key.empty?) then
507
+ raise SyntaxError, 'unexpected end of search key.'
508
+ end
509
+
510
+ op = search_key.shift
511
+ op = op.upcase if (op.is_a? String)
512
+
513
+ case (op)
514
+ when 'ALL'
515
+ factory = parse_all
516
+ when 'ANSWERED'
517
+ factory = parse_msg_flag_enabled('answered')
518
+ when 'BCC'
519
+ search_string = shift_string_value('BCC', search_key)
520
+ factory = parse_search_header('bcc', search_string)
521
+ when 'BEFORE'
522
+ search_date = shift_date_value('BEFORE', search_key)
523
+ factory = parse_internal_date(search_date) {|d, boundary| d < boundary }
524
+ when 'BODY'
525
+ search_string = shift_string_value('BODY', search_key)
526
+ factory = parse_body(search_string)
527
+ when 'CC'
528
+ search_string = shift_string_value('CC', search_key)
529
+ factory = parse_search_header('cc', search_string)
530
+ when 'DELETED'
531
+ factory = parse_msg_flag_enabled('deleted')
532
+ when 'DRAFT'
533
+ factory = parse_msg_flag_enabled('draft')
534
+ when 'FLAGGED'
535
+ factory = parse_msg_flag_enabled('flagged')
536
+ when 'FROM'
537
+ search_string = shift_string_value('FROM', search_key)
538
+ factory = parse_search_header('from', search_string)
539
+ when 'HEADER'
540
+ header_name = shift_string_value('HEADER', search_key)
541
+ search_string = shift_string_value('HEADER', search_key)
542
+ factory = parse_search_header(header_name, search_string)
543
+ when 'KEYWORD'
544
+ search_string = shift_string_value('KEYWORD', search_key)
545
+ factory = parse_keyword(search_string)
546
+ when 'LARGER'
547
+ octet_size = shift_octet_size_value('LARGER', search_key)
548
+ factory = parse_mail_bytesize(octet_size) {|size, boundary| size > boundary }
549
+ when 'NEW'
550
+ factory = parse_new
551
+ when 'NOT'
552
+ next_node = fetch_next_node(search_key)
553
+ factory = parse_not(next_node)
554
+ when 'OLD'
555
+ factory = parse_old
556
+ when 'ON'
557
+ search_date = shift_date_value('ON', search_key)
558
+ factory = parse_internal_date(search_date) {|d, boundary| d == boundary }
559
+ when 'OR'
560
+ next_node1 = fetch_next_node(search_key)
561
+ next_node2 = fetch_next_node(search_key)
562
+ factory = parse_or(next_node1, next_node2)
563
+ when 'RECENT'
564
+ factory = parse_msg_flag_enabled('recent')
565
+ when 'SEEN'
566
+ factory = parse_msg_flag_enabled('seen')
567
+ when 'SENTBEFORE'
568
+ search_date = shift_date_value('SENTBEFORE', search_key)
569
+ factory = parse_mail_date(search_date) {|d, boundary| d < boundary }
570
+ when 'SENTON'
571
+ search_date = shift_date_value('SENTON', search_key)
572
+ factory = parse_mail_date(search_date) {|d, boundary| d == boundary }
573
+ when 'SENTSINCE'
574
+ search_date = shift_date_value('SENTSINCE', search_key)
575
+ factory = parse_mail_date(search_date) {|d, boundary| d > boundary }
576
+ when 'SINCE'
577
+ search_date = shift_date_value('SINCE', search_key)
578
+ factory = parse_internal_date(search_date) {|d, boundary| d > boundary }
579
+ when 'SMALLER'
580
+ octet_size = shift_octet_size_value('SMALLER', search_key)
581
+ factory = parse_mail_bytesize(octet_size) {|size, boundary| size < boundary }
582
+ when 'SUBJECT'
583
+ search_string = shift_string_value('SUBJECT', search_key)
584
+ factory = parse_search_header('subject', search_string)
585
+ when 'TEXT'
586
+ search_string = shift_string_value('TEXT', search_key)
587
+ factory = parse_text(search_string)
588
+ when 'TO'
589
+ search_string = shift_string_value('TO', search_key)
590
+ factory = parse_search_header('to', search_string)
591
+ when 'UID'
592
+ mset_string = shift_string_value('UID', search_key)
593
+ msg_set = @folder.parse_msg_set(mset_string, uid: true)
594
+ factory = parse_uid(msg_set)
595
+ when 'UNANSWERED'
596
+ factory = parse_msg_flag_disabled('answered')
597
+ when 'UNDELETED'
598
+ factory = parse_msg_flag_disabled('deleted')
599
+ when 'UNDRAFT'
600
+ factory = parse_msg_flag_disabled('draft')
601
+ when 'UNFLAGGED'
602
+ factory = parse_msg_flag_disabled('flagged')
603
+ when 'UNKEYWORD'
604
+ search_string = shift_string_value('UNKEYWORD', search_key)
605
+ factory = parse_unkeyword(search_string)
606
+ when 'UNSEEN'
607
+ factory = parse_msg_flag_disabled('seen')
608
+ when String
609
+ begin
610
+ msg_set = @folder.parse_msg_set(op, uid: false)
611
+ factory = parse_msg_set(msg_set)
612
+ rescue MessageSetSyntaxError
613
+ raise SyntaxError, "unknown search key: #{op}"
614
+ end
615
+ when Array
616
+ case (op[0])
617
+ when :group
618
+ factory = parse_group(op[1..-1])
619
+ else
620
+ raise SyntaxError, "unknown search key: #{op}"
621
+ end
622
+ else
623
+ raise SyntaxError, "unknown search key: #{op}"
624
+ end
625
+
626
+ factory
627
+ end
628
+ private :fetch_next_node
629
+
630
+ def parse_cached(search_key)
631
+ unless (search_key.empty?) then
632
+ search_key = search_key.dup
633
+ factory = fetch_next_node(search_key)
634
+ cond = factory.call(parse_cached(search_key))
635
+ else
636
+ cond = end_of_cond
637
+ end
638
+ end
639
+ private :parse_cached
640
+
641
+ def parse(search_key)
642
+ cond = parse_cached(search_key)
643
+ proc{|msg|
644
+ found = cond.call(msg)
645
+ @mail_cache.clear
646
+ found
647
+ }
648
+ end
649
+ end
650
+
651
+ class FetchParser
652
+ module Utils
653
+ def encode_list(array)
654
+ '('.b << array.map{|v|
655
+ case (v)
656
+ when Symbol
657
+ v.to_s
658
+ when String
659
+ Protocol.quote(v)
660
+ when Integer
661
+ v.to_s
662
+ when NilClass
663
+ 'NIL'
664
+ when Array
665
+ encode_list(v)
666
+ else
667
+ raise "unknown value: #{v}"
668
+ end
669
+ }.join(' '.b) << ')'.b
670
+ end
671
+ module_function :encode_list
672
+
673
+ def encode_header(name_value_pair_list)
674
+ name_value_pair_list.map{|n, v| ''.b << n << ': ' << v << "\r\n" }.join('') << "\r\n"
675
+ end
676
+ module_function :encode_header
677
+
678
+ def get_body_section(mail, index_list)
679
+ if (index_list.empty?) then
680
+ mail
681
+ else
682
+ i, *next_index_list = index_list
683
+ unless (i > 0) then
684
+ raise SyntaxError, "not a none-zero body section number: #{i}"
685
+ end
686
+ if (mail.multipart?) then
687
+ get_body_section(mail.parts[i - 1], next_index_list)
688
+ elsif (mail.message?) then
689
+ get_body_section(mail.message, index_list)
690
+ else
691
+ if (i == 1) then
692
+ if (next_index_list.empty?) then
693
+ mail
694
+ else
695
+ nil
696
+ end
697
+ else
698
+ nil
699
+ end
700
+ end
701
+ end
702
+ end
703
+ module_function :get_body_section
704
+
705
+ def get_body_content(mail, name, nest_mail: false)
706
+ if (nest_mail) then
707
+ if (mail.message?) then
708
+ mail.message.__send__(name)
709
+ else
710
+ nil
711
+ end
712
+ else
713
+ mail.__send__(name)
714
+ end
715
+ end
716
+ module_function :get_body_content
717
+ end
718
+ include Utils
719
+
720
+ def initialize(mail_store, folder)
721
+ @mail_store = mail_store
722
+ @folder = folder
723
+ @charset = nil
724
+ @mail_cache = Hash.new{|hash, uid|
725
+ if (msg_txt = @mail_store.msg_text(@folder.mbox_id, uid)) then
726
+ hash[uid] = RFC822::Message.new(msg_txt)
727
+ end
728
+ }
729
+ end
730
+
731
+ def get_mail(msg)
732
+ @mail_cache[msg.uid] or raise "not found a mail: #{msg.uid}"
733
+ end
734
+ private :get_mail
735
+
736
+ def make_array(value)
737
+ if (value) then
738
+ if (value.is_a? Array) then
739
+ list = value
740
+ else
741
+ list = [ value ]
742
+ end
743
+
744
+ if (block_given?) then
745
+ yield(list)
746
+ else
747
+ list
748
+ end
749
+ end
750
+ end
751
+ private :make_array
752
+
753
+ def make_address_list(email_address)
754
+ mailbox, host = email_address.split(/@/, 2)
755
+ [ nil, nil, mailbox, host ]
756
+ end
757
+ private :make_address_list
758
+
759
+ def expand_macro(cmd_list)
760
+ func_list = cmd_list.map{|name| parse_cached(name) }
761
+ proc{|msg|
762
+ func_list.map{|f| f.call(msg) }.join(' '.b)
763
+ }
764
+ end
765
+ private :expand_macro
766
+
767
+ def get_header_field(mail, name, default=nil)
768
+ if (field = mail[name]) then
769
+ if (block_given?) then
770
+ yield(field)
771
+ else
772
+ field
773
+ end
774
+ else
775
+ default
776
+ end
777
+ end
778
+ private :get_header_field
779
+
780
+ def get_bodystructure_data(mail)
781
+ if (mail.multipart?) then # body_type_mpart
782
+ mpart_data = []
783
+ mpart_data.concat(mail.parts.map{|part_msg| get_bodystructure_data(part_msg) })
784
+ mpart_data << mail.media_sub_type_upcase
785
+ elsif (mail.text?) then # body_type_text
786
+ text_data = []
787
+
788
+ # media_text
789
+ text_data << mail.media_main_type_upcase
790
+ text_data << mail.media_sub_type_upcase
791
+
792
+ # body_fields
793
+ text_data << mail.content_type_parameters.flatten
794
+ text_data << mail.header['Content-Id']
795
+ text_data << mail.header['Content-Description']
796
+ text_data << mail.header.fetch_upcase('Content-Transfer-Encoding')
797
+ text_data << mail.raw_source.bytesize
798
+
799
+ # body_fld_lines
800
+ text_data << mail.raw_source.each_line.count
801
+ elsif (mail.message?) then # body_type_msg
802
+ msg_data = []
803
+
804
+ # message_media
805
+ msg_data << mail.media_main_type_upcase
806
+ msg_data << mail.media_sub_type_upcase
807
+
808
+ # body_fields
809
+ msg_data << mail.content_type_parameters.flatten
810
+ msg_data << mail.header['Content-Id']
811
+ msg_data << mail.header['Content-Description']
812
+ msg_data << mail.header.fetch_upcase('Content-Transfer-Encoding')
813
+ msg_data << mail.raw_source.bytesize
814
+
815
+ # envelope
816
+ msg_data << get_envelope_data(mail.message)
817
+
818
+ # body
819
+ msg_data << get_bodystructure_data(mail.message)
820
+
821
+ # body_fld_lines
822
+ msg_data << mail.raw_source.each_line.count
823
+ else # body_type_basic
824
+ basic_data = []
825
+
826
+ # media_basic
827
+ basic_data << mail.media_main_type_upcase
828
+ basic_data << mail.media_sub_type_upcase
829
+
830
+ # body_fields
831
+ basic_data << mail.content_type_parameters.flatten
832
+ basic_data << mail.header['Content-Id']
833
+ basic_data << mail.header['Content-Description']
834
+ basic_data << mail.header.fetch_upcase('Content-Transfer-Encoding')
835
+ basic_data << mail.raw_source.bytesize
836
+ end
837
+ end
838
+ private :get_bodystructure_data
839
+
840
+ def get_envelope_data(mail)
841
+ env_data = []
842
+ env_data << mail.header['Date']
843
+ env_data << mail.header['Subject']
844
+ env_data << mail.from
845
+ env_data << mail.sender
846
+ env_data << mail.reply_to
847
+ env_data << mail.to
848
+ env_data << mail.cc
849
+ env_data << mail.bcc
850
+ env_data << mail.header['In-Reply-To']
851
+ env_data << mail.header['Message-Id']
852
+ end
853
+ private :get_envelope_data
854
+
855
+ def parse_body(body, msg_att_name)
856
+ enable_seen = true
857
+ if (body.option) then
858
+ case (body.option.upcase)
859
+ when 'PEEK'
860
+ enable_seen = false
861
+ else
862
+ raise SyntaxError, "unknown fetch body option: #{option}"
863
+ end
864
+ end
865
+ if (@folder.read_only?) then
866
+ enable_seen = false
867
+ end
868
+
869
+ if (enable_seen) then
870
+ fetch_flags = parse_flags('FLAGS')
871
+ fetch_flags_changed = proc{|msg|
872
+ unless (@mail_store.msg_flag(@folder.mbox_id, msg.uid, 'seen')) then
873
+ @mail_store.set_msg_flag(@folder.mbox_id, msg.uid, 'seen', true)
874
+ fetch_flags.call(msg) + ' '.b
875
+ else
876
+ ''.b
877
+ end
878
+ }
879
+ else
880
+ fetch_flags_changed = proc{|msg|
881
+ ''.b
882
+ }
883
+ end
884
+
885
+ if (body.section_list.empty?) then
886
+ section_text = nil
887
+ section_index_list = []
888
+ else
889
+ if (body.section_list[0] =~ /\A(?<index>\d+(?:\.\d+)*)(?:\.(?<text>.+))?\z/) then
890
+ section_text = $~[:text]
891
+ section_index_list = $~[:index].split(/\./).map{|i| i.to_i }
892
+ else
893
+ section_text = body.section_list[0]
894
+ section_index_list = []
895
+ end
896
+ end
897
+
898
+ is_root = section_index_list.empty?
899
+ unless (section_text) then
900
+ if (is_root) then
901
+ fetch_body_content = proc{|mail|
902
+ mail.raw_source
903
+ }
904
+ else
905
+ fetch_body_content = proc{|mail|
906
+ mail.body.raw_source
907
+ }
908
+ end
909
+ else
910
+ section_text = section_text.upcase
911
+ case (section_text)
912
+ when 'MIME'
913
+ if (section_index_list.empty?) then
914
+ raise SyntaxError, "need for section index at #{section_text}."
915
+ else
916
+ fetch_body_content = proc{|mail|
917
+ if (header = get_body_content(mail, :header)) then
918
+ header.raw_source
919
+ end
920
+ }
921
+ end
922
+ when 'HEADER'
923
+ fetch_body_content = proc{|mail|
924
+ if (header = get_body_content(mail, :header, nest_mail: ! is_root)) then
925
+ header.raw_source
926
+ end
927
+ }
928
+ when 'HEADER.FIELDS', 'HEADER.FIELDS.NOT'
929
+ if (body.section_list.length != 2) then
930
+ raise SyntaxError, "need for argument of #{section_text}."
931
+ end
932
+ field_name_list = body.section_list[1]
933
+ unless ((field_name_list.is_a? Array) && (field_name_list[0] == :group)) then
934
+ raise SyntaxError, "invalid argument of #{section_text}: #{field_name_list}"
935
+ end
936
+ field_name_list = field_name_list[1..-1]
937
+ case (section_text)
938
+ when 'HEADER.FIELDS'
939
+ fetch_body_content = proc{|mail|
940
+ if (header = get_body_content(mail, :header, nest_mail: ! is_root)) then
941
+ field_name_set = field_name_list.map{|n| n.downcase }.to_set
942
+ name_value_pair_list = header.select{|n, v| field_name_set.include? n.downcase }
943
+ encode_header(name_value_pair_list)
944
+ end
945
+ }
946
+ when 'HEADER.FIELDS.NOT'
947
+ fetch_body_content = proc{|mail|
948
+ if (header = get_body_content(mail, :header, nest_mail: ! is_root)) then
949
+ field_name_set = field_name_list.map{|n| n.downcase }.to_set
950
+ name_value_pair_list = header.reject{|n, v| field_name_set.include? n.downcase }
951
+ encode_header(name_value_pair_list)
952
+ end
953
+ }
954
+ else
955
+ raise 'internal error.'
956
+ end
957
+ when 'TEXT'
958
+ fetch_body_content = proc{|mail|
959
+ if (mail_body = get_body_content(mail, :body, nest_mail: ! is_root)) then
960
+ mail_body.raw_source
961
+ end
962
+ }
963
+ else
964
+ raise SyntaxError, "unknown fetch body section text: #{section_text}"
965
+ end
966
+ end
967
+
968
+ proc{|msg|
969
+ res = ''.b
970
+ res << fetch_flags_changed.call(msg)
971
+ res << msg_att_name
972
+ res << ' '.b
973
+
974
+ mail = get_body_section(get_mail(msg), section_index_list)
975
+ content = fetch_body_content.call(mail) if mail
976
+ if (content) then
977
+ if (body.partial_origin) then
978
+ if (content.bytesize > body.partial_origin) then
979
+ partial_content = content.byteslice((body.partial_origin)..-1)
980
+ if (partial_content.bytesize > body.partial_size) then # because bignum byteslice is failed.
981
+ partial_content = partial_content.byteslice(0, body.partial_size)
982
+ end
983
+ res << Protocol.quote(partial_content)
984
+ else
985
+ res << 'NIL'.b
986
+ end
987
+ else
988
+ res << Protocol.quote(content)
989
+ end
990
+ else
991
+ res << 'NIL'.b
992
+ end
993
+ }
994
+ end
995
+ private :parse_body
996
+
997
+ def parse_bodystructure(msg_att_name)
998
+ proc{|msg|
999
+ ''.b << msg_att_name << ' '.b << encode_list(get_bodystructure_data(get_mail(msg)))
1000
+ }
1001
+ end
1002
+ private :parse_bodystructure
1003
+
1004
+ def parse_envelope(msg_att_name)
1005
+ proc{|msg|
1006
+ ''.b << msg_att_name << ' '.b << encode_list(get_envelope_data(get_mail(msg)))
1007
+ }
1008
+ end
1009
+ private :parse_envelope
1010
+
1011
+ def parse_flags(msg_att_name)
1012
+ proc{|msg|
1013
+ flag_list = MailStore::MSG_FLAG_NAMES.find_all{|flag_name|
1014
+ @mail_store.msg_flag(@folder.mbox_id, msg.uid, flag_name)
1015
+ }.map{|flag_name|
1016
+ "\\".b << flag_name.capitalize
1017
+ }.join(' ')
1018
+ ''.b << msg_att_name << ' (' << flag_list << ')'
1019
+ }
1020
+ end
1021
+ private :parse_flags
1022
+
1023
+ def parse_internaldate(msg_att_name)
1024
+ proc{|msg|
1025
+ ''.b << msg_att_name << @mail_store.msg_date(@folder.mbox_id, msg.uid).strftime(' "%d-%b-%Y %H:%M:%S %z"'.b)
1026
+ }
1027
+ end
1028
+ private :parse_internaldate
1029
+
1030
+ def parse_rfc822_size(msg_att_name)
1031
+ proc{|msg|
1032
+ ''.b << msg_att_name << ' '.b << get_mail(msg).raw_source.bytesize.to_s
1033
+ }
1034
+ end
1035
+ private :parse_rfc822_size
1036
+
1037
+ def parse_uid(msg_att_name)
1038
+ proc{|msg|
1039
+ ''.b << msg_att_name << ' '.b << msg.uid.to_s
1040
+ }
1041
+ end
1042
+ private :parse_uid
1043
+
1044
+ def parse_group(fetch_attrs)
1045
+ group_fetch_list = fetch_attrs.map{|fetch_att| parse_cached(fetch_att) }
1046
+ proc{|msg|
1047
+ '('.b << group_fetch_list.map{|fetch| fetch.call(msg) }.join(' '.b) << ')'.b
1048
+ }
1049
+ end
1050
+ private :parse_group
1051
+
1052
+ def parse_cached(fetch_att)
1053
+ fetch_att = fetch_att.upcase if (fetch_att.is_a? String)
1054
+ case (fetch_att)
1055
+ when 'ALL'
1056
+ fetch = expand_macro(%w[ FLAGS INTERNALDATE RFC822.SIZE ENVELOPE ])
1057
+ when 'BODY'
1058
+ fetch = parse_bodystructure(fetch_att)
1059
+ when 'BODYSTRUCTURE'
1060
+ fetch = parse_bodystructure(fetch_att)
1061
+ when 'ENVELOPE'
1062
+ fetch = parse_envelope(fetch_att)
1063
+ when 'FAST'
1064
+ fetch = expand_macro(%w[ FLAGS INTERNALDATE RFC822.SIZE ])
1065
+ when 'FLAGS'
1066
+ fetch = parse_flags(fetch_att)
1067
+ when 'FULL'
1068
+ fetch = expand_macro(%w[ FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY ])
1069
+ when 'INTERNALDATE'
1070
+ fetch = parse_internaldate(fetch_att)
1071
+ when 'RFC822'
1072
+ fetch = parse_body(Protocol.body(section_list: []), fetch_att)
1073
+ when 'RFC822.HEADER'
1074
+ fetch = parse_body(Protocol.body(option: 'PEEK', section_list: %w[ HEADER ]), fetch_att)
1075
+ when 'RFC822.SIZE'
1076
+ fetch = parse_rfc822_size(fetch_att)
1077
+ when 'RFC822.TEXT'
1078
+ fetch = parse_body(Protocol.body(section_list: %w[ TEXT ]), fetch_att)
1079
+ when 'UID'
1080
+ fetch = parse_uid(fetch_att)
1081
+ when Array
1082
+ case (fetch_att[0])
1083
+ when :group
1084
+ fetch = parse_group(fetch_att[1..-1])
1085
+ when :body
1086
+ body = fetch_att[1]
1087
+ fetch = parse_body(body, body.msg_att_name)
1088
+ else
1089
+ raise SyntaxError, "unknown fetch attribute: #{fetch_att[0]}"
1090
+ end
1091
+ else
1092
+ raise SyntaxError, "unknown fetch attribute: #{fetch_att}"
1093
+ end
1094
+
1095
+ fetch
1096
+ end
1097
+ private :parse_cached
1098
+
1099
+ def parse(fetch_att)
1100
+ fetch = parse_cached(fetch_att)
1101
+ proc{|msg|
1102
+ res = fetch.call(msg)
1103
+ @mail_cache.clear
1104
+ res
1105
+ }
1106
+ end
1107
+ end
1108
+ end
1109
+ end
1110
+
1111
+ # Local Variables:
1112
+ # mode: Ruby
1113
+ # indent-tabs-mode: nil
1114
+ # End: