rims 0.2.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.
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: