postman 0.1.0

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.
@@ -0,0 +1,4 @@
1
+ postman
2
+ =======
3
+
4
+ Postman is a ruby daemon for fetching mail for an address via pop3 or imap and then delivering it to a specified webhook. There is a sinatra web front end so you can see what's going on.
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/ruby
2
+ $:.unshift('./lib') # use this during development
3
+ require 'rubygems'
4
+ require 'postman'
5
+ require 'daemons'
6
+ require 'logger'
7
+
8
+ Daemons.run_proc('postman', :multiple => true) do
9
+
10
+ config_file = ARGV[1]
11
+ unless config_file
12
+ puts "Usage: postman [run|start|stop] <config_file>"
13
+ exit(0)
14
+ end
15
+
16
+ config = YAML.load_file(config_file)
17
+ logger = Logger.new(config['log'])
18
+
19
+ logger.info "Postman: using config file #{config_file}"
20
+ logger.info "The postman is starting his rounds..."
21
+
22
+ postman = Postman::Runner.new(config_file)
23
+
24
+ trap('INT') do
25
+ logger.info "INT: Letting the postman finish his rounds..."
26
+ postman.stop
27
+ end
28
+
29
+ trap('TERM') do
30
+ logger.info "TERM: Letting the postman finish his rounds..."
31
+ postman.stop
32
+ end
33
+
34
+ trap(:QUIT) do
35
+ logger.info "QUIT: Letting the postman finish his rounds..."
36
+ postman.stop
37
+ end
38
+
39
+ postman.start
40
+
41
+ logger.info "Post round is over, time for a cuppa."
42
+ end
@@ -0,0 +1,991 @@
1
+ # = net/pop.rb
2
+ #
3
+ # Copyright (c) 1999-2007 Yukihiro Matsumoto.
4
+ #
5
+ # Copyright (c) 1999-2007 Minero Aoki.
6
+ #
7
+ # Written & maintained by Minero Aoki <aamine@loveruby.net>.
8
+ #
9
+ # Documented by William Webber and Minero Aoki.
10
+ #
11
+ # This program is free software. You can re-distribute and/or
12
+ # modify this program under the same terms as Ruby itself,
13
+ # Ruby Distribute License.
14
+ #
15
+ # NOTE: You can find Japanese version of this document at:
16
+ # http://www.ruby-lang.org/ja/man/index.cgi?cmd=view;name=net%2Fpop.rb
17
+ #
18
+ # $Id$
19
+ #
20
+ # See Net::POP3 for documentation.
21
+ #
22
+
23
+ require 'net/protocol'
24
+ require 'digest/md5'
25
+ require 'timeout'
26
+
27
+ begin
28
+ require "openssl"
29
+ rescue LoadError
30
+ end
31
+
32
+ module Net
33
+
34
+ # Non-authentication POP3 protocol error
35
+ # (reply code "-ERR", except authentication).
36
+ class POPError < ProtocolError; end
37
+
38
+ # POP3 authentication error.
39
+ class POPAuthenticationError < ProtoAuthError; end
40
+
41
+ # Unexpected response from the server.
42
+ class POPBadResponse < POPError; end
43
+
44
+ #
45
+ # = Net::POP3
46
+ #
47
+ # == What is This Library?
48
+ #
49
+ # This library provides functionality for retrieving
50
+ # email via POP3, the Post Office Protocol version 3. For details
51
+ # of POP3, see [RFC1939] (http://www.ietf.org/rfc/rfc1939.txt).
52
+ #
53
+ # == Examples
54
+ #
55
+ # === Retrieving Messages
56
+ #
57
+ # This example retrieves messages from the server and deletes them
58
+ # on the server.
59
+ #
60
+ # Messages are written to files named 'inbox/1', 'inbox/2', ....
61
+ # Replace 'pop.example.com' with your POP3 server address, and
62
+ # 'YourAccount' and 'YourPassword' with the appropriate account
63
+ # details.
64
+ #
65
+ # require 'net/pop'
66
+ #
67
+ # pop = Net::POP3.new('pop.example.com')
68
+ # pop.start('YourAccount', 'YourPassword') # (1)
69
+ # if pop.mails.empty?
70
+ # puts 'No mail.'
71
+ # else
72
+ # i = 0
73
+ # pop.each_mail do |m| # or "pop.mails.each ..." # (2)
74
+ # File.open("inbox/#{i}", 'w') do |f|
75
+ # f.write m.pop
76
+ # end
77
+ # m.delete
78
+ # i += 1
79
+ # end
80
+ # puts "#{pop.mails.size} mails popped."
81
+ # end
82
+ # pop.finish # (3)
83
+ #
84
+ # 1. Call Net::POP3#start and start POP session.
85
+ # 2. Access messages by using POP3#each_mail and/or POP3#mails.
86
+ # 3. Close POP session by calling POP3#finish or use the block form of #start.
87
+ #
88
+ # === Shortened Code
89
+ #
90
+ # The example above is very verbose. You can shorten the code by using
91
+ # some utility methods. First, the block form of Net::POP3.start can
92
+ # be used instead of POP3.new, POP3#start and POP3#finish.
93
+ #
94
+ # require 'net/pop'
95
+ #
96
+ # Net::POP3.start('pop.example.com', 110,
97
+ # 'YourAccount', 'YourPassword') do |pop|
98
+ # if pop.mails.empty?
99
+ # puts 'No mail.'
100
+ # else
101
+ # i = 0
102
+ # pop.each_mail do |m| # or "pop.mails.each ..."
103
+ # File.open("inbox/#{i}", 'w') do |f|
104
+ # f.write m.pop
105
+ # end
106
+ # m.delete
107
+ # i += 1
108
+ # end
109
+ # puts "#{pop.mails.size} mails popped."
110
+ # end
111
+ # end
112
+ #
113
+ # POP3#delete_all is an alternative for #each_mail and #delete.
114
+ #
115
+ # require 'net/pop'
116
+ #
117
+ # Net::POP3.start('pop.example.com', 110,
118
+ # 'YourAccount', 'YourPassword') do |pop|
119
+ # if pop.mails.empty?
120
+ # puts 'No mail.'
121
+ # else
122
+ # i = 1
123
+ # pop.delete_all do |m|
124
+ # File.open("inbox/#{i}", 'w') do |f|
125
+ # f.write m.pop
126
+ # end
127
+ # i += 1
128
+ # end
129
+ # end
130
+ # end
131
+ #
132
+ # And here is an even shorter example.
133
+ #
134
+ # require 'net/pop'
135
+ #
136
+ # i = 0
137
+ # Net::POP3.delete_all('pop.example.com', 110,
138
+ # 'YourAccount', 'YourPassword') do |m|
139
+ # File.open("inbox/#{i}", 'w') do |f|
140
+ # f.write m.pop
141
+ # end
142
+ # i += 1
143
+ # end
144
+ #
145
+ # === Memory Space Issues
146
+ #
147
+ # All the examples above get each message as one big string.
148
+ # This example avoids this.
149
+ #
150
+ # require 'net/pop'
151
+ #
152
+ # i = 1
153
+ # Net::POP3.delete_all('pop.example.com', 110,
154
+ # 'YourAccount', 'YourPassword') do |m|
155
+ # File.open("inbox/#{i}", 'w') do |f|
156
+ # m.pop do |chunk| # get a message little by little.
157
+ # f.write chunk
158
+ # end
159
+ # i += 1
160
+ # end
161
+ # end
162
+ #
163
+ # === Using APOP
164
+ #
165
+ # The net/pop library supports APOP authentication.
166
+ # To use APOP, use the Net::APOP class instead of the Net::POP3 class.
167
+ # You can use the utility method, Net::POP3.APOP(). For example:
168
+ #
169
+ # require 'net/pop'
170
+ #
171
+ # # Use APOP authentication if $isapop == true
172
+ # pop = Net::POP3.APOP($is_apop).new('apop.example.com', 110)
173
+ # pop.start(YourAccount', 'YourPassword') do |pop|
174
+ # # Rest of the code is the same.
175
+ # end
176
+ #
177
+ # === Fetch Only Selected Mail Using 'UIDL' POP Command
178
+ #
179
+ # If your POP server provides UIDL functionality,
180
+ # you can grab only selected mails from the POP server.
181
+ # e.g.
182
+ #
183
+ # def need_pop?( id )
184
+ # # determine if we need pop this mail...
185
+ # end
186
+ #
187
+ # Net::POP3.start('pop.example.com', 110,
188
+ # 'Your account', 'Your password') do |pop|
189
+ # pop.mails.select { |m| need_pop?(m.unique_id) }.each do |m|
190
+ # do_something(m.pop)
191
+ # end
192
+ # end
193
+ #
194
+ # The POPMail#unique_id() method returns the unique-id of the message as a
195
+ # String. Normally the unique-id is a hash of the message.
196
+ #
197
+ class POP3 < Protocol
198
+
199
+ Revision = %q$Revision$.split[1]
200
+
201
+ #
202
+ # Class Parameters
203
+ #
204
+
205
+ def POP3.default_port
206
+ default_pop3_port()
207
+ end
208
+
209
+ # The default port for POP3 connections, port 110
210
+ def POP3.default_pop3_port
211
+ 110
212
+ end
213
+
214
+ # The default port for POP3S connections, port 995
215
+ def POP3.default_pop3s_port
216
+ 995
217
+ end
218
+
219
+ def POP3.socket_type #:nodoc: obsolete
220
+ Net::InternetMessageIO
221
+ end
222
+
223
+ #
224
+ # Utilities
225
+ #
226
+
227
+ # Returns the APOP class if +isapop+ is true; otherwise, returns
228
+ # the POP class. For example:
229
+ #
230
+ # # Example 1
231
+ # pop = Net::POP3::APOP($is_apop).new(addr, port)
232
+ #
233
+ # # Example 2
234
+ # Net::POP3::APOP($is_apop).start(addr, port) do |pop|
235
+ # ....
236
+ # end
237
+ #
238
+ def POP3.APOP(isapop)
239
+ isapop ? APOP : POP3
240
+ end
241
+
242
+ # Starts a POP3 session and iterates over each POPMail object,
243
+ # yielding it to the +block+.
244
+ # This method is equivalent to:
245
+ #
246
+ # Net::POP3.start(address, port, account, password) do |pop|
247
+ # pop.each_mail do |m|
248
+ # yield m
249
+ # end
250
+ # end
251
+ #
252
+ # This method raises a POPAuthenticationError if authentication fails.
253
+ #
254
+ # === Example
255
+ #
256
+ # Net::POP3.foreach('pop.example.com', 110,
257
+ # 'YourAccount', 'YourPassword') do |m|
258
+ # file.write m.pop
259
+ # m.delete if $DELETE
260
+ # end
261
+ #
262
+ def POP3.foreach(address, port = nil,
263
+ account = nil, password = nil,
264
+ isapop = false, &block) # :yields: message
265
+ start(address, port, account, password, isapop) {|pop|
266
+ pop.each_mail(&block)
267
+ }
268
+ end
269
+
270
+ # Starts a POP3 session and deletes all messages on the server.
271
+ # If a block is given, each POPMail object is yielded to it before
272
+ # being deleted.
273
+ #
274
+ # This method raises a POPAuthenticationError if authentication fails.
275
+ #
276
+ # === Example
277
+ #
278
+ # Net::POP3.delete_all('pop.example.com', 110,
279
+ # 'YourAccount', 'YourPassword') do |m|
280
+ # file.write m.pop
281
+ # end
282
+ #
283
+ def POP3.delete_all(address, port = nil,
284
+ account = nil, password = nil,
285
+ isapop = false, &block)
286
+ start(address, port, account, password, isapop) {|pop|
287
+ pop.delete_all(&block)
288
+ }
289
+ end
290
+
291
+ # Opens a POP3 session, attempts authentication, and quits.
292
+ #
293
+ # This method raises POPAuthenticationError if authentication fails.
294
+ #
295
+ # === Example: normal POP3
296
+ #
297
+ # Net::POP3.auth_only('pop.example.com', 110,
298
+ # 'YourAccount', 'YourPassword')
299
+ #
300
+ # === Example: APOP
301
+ #
302
+ # Net::POP3.auth_only('pop.example.com', 110,
303
+ # 'YourAccount', 'YourPassword', true)
304
+ #
305
+ def POP3.auth_only(address, port = nil,
306
+ account = nil, password = nil,
307
+ isapop = false)
308
+ new(address, port, isapop).auth_only account, password
309
+ end
310
+
311
+ # Starts a pop3 session, attempts authentication, and quits.
312
+ # This method must not be called while POP3 session is opened.
313
+ # This method raises POPAuthenticationError if authentication fails.
314
+ def auth_only(account, password)
315
+ raise IOError, 'opening previously opened POP session' if started?
316
+ start(account, password) {
317
+ ;
318
+ }
319
+ end
320
+
321
+ #
322
+ # SSL
323
+ #
324
+
325
+ @use_ssl = false
326
+ @verify = nil
327
+ @certs = nil
328
+
329
+ # Enable SSL for all new instances.
330
+ # +verify+ is the type of verification to do on the Server Cert; Defaults
331
+ # to OpenSSL::SSL::VERIFY_NONE.
332
+ # +certs+ is a file or directory holding CA certs to use to verify the
333
+ # server cert; Defaults to nil.
334
+ def POP3.enable_ssl(verify = OpenSSL::SSL::VERIFY_NONE, certs = nil)
335
+ @use_ssl = true
336
+ @verify = verify
337
+ @certs = certs
338
+ end
339
+
340
+ # Disable SSL for all new instances.
341
+ def POP3.disable_ssl
342
+ @use_ssl = nil
343
+ @verify = nil
344
+ @certs = nil
345
+ end
346
+
347
+ def POP3.use_ssl?
348
+ @use_ssl
349
+ end
350
+
351
+ def POP3.verify
352
+ @verify
353
+ end
354
+
355
+ def POP3.certs
356
+ @certs
357
+ end
358
+
359
+ #
360
+ # Session management
361
+ #
362
+
363
+ # Creates a new POP3 object and open the connection. Equivalent to
364
+ #
365
+ # Net::POP3.new(address, port, isapop).start(account, password)
366
+ #
367
+ # If +block+ is provided, yields the newly-opened POP3 object to it,
368
+ # and automatically closes it at the end of the session.
369
+ #
370
+ # === Example
371
+ #
372
+ # Net::POP3.start(addr, port, account, password) do |pop|
373
+ # pop.each_mail do |m|
374
+ # file.write m.pop
375
+ # m.delete
376
+ # end
377
+ # end
378
+ #
379
+ def POP3.start(address, port = nil,
380
+ account = nil, password = nil,
381
+ isapop = false, &block) # :yield: pop
382
+ new(address, port, isapop).start(account, password, &block)
383
+ end
384
+
385
+ # Creates a new POP3 object.
386
+ #
387
+ # +address+ is the hostname or ip address of your POP3 server.
388
+ #
389
+ # The optional +port+ is the port to connect to.
390
+ #
391
+ # The optional +isapop+ specifies whether this connection is going
392
+ # to use APOP authentication; it defaults to +false+.
393
+ #
394
+ # This method does *not* open the TCP connection.
395
+ def initialize(addr, port = nil, isapop = false)
396
+ @address = addr
397
+ @use_ssl = POP3.use_ssl?
398
+ @port = port || (POP3.use_ssl? ? POP3.default_pop3s_port : POP3.default_pop3_port)
399
+ @apop = isapop
400
+ @certs = POP3.certs
401
+ @verify = POP3.verify
402
+
403
+ @command = nil
404
+ @socket = nil
405
+ @started = false
406
+ @open_timeout = 30
407
+ @read_timeout = 60
408
+ @debug_output = nil
409
+
410
+ @mails = nil
411
+ @n_mails = nil
412
+ @n_bytes = nil
413
+ end
414
+
415
+ # Does this instance use APOP authentication?
416
+ def apop?
417
+ @apop
418
+ end
419
+
420
+ # does this instance use SSL?
421
+ def use_ssl?
422
+ @use_ssl
423
+ end
424
+
425
+ # Enables SSL for this instance. Must be called before the connection is
426
+ # established to have any effect.
427
+ # +verify+ is the type of verification to do on the Server Cert; Defaults
428
+ # to OpenSSL::SSL::VERIFY_NONE.
429
+ # +certs+ is a file or directory holding CA certs to use to verify the
430
+ # server cert; Defaults to nil.
431
+ # +port+ is port to establish the SSL connection on; Defaults to 995.
432
+ def enable_ssl(verify = OpenSSL::SSL::VERIFY_NONE, certs = nil,
433
+ port = POP3.default_pop3s_port)
434
+ @use_ssl = true
435
+ @verify = verify
436
+ @certs = certs
437
+ @port = port
438
+ end
439
+
440
+ def disable_ssl
441
+ @use_ssl = false
442
+ @verify = nil
443
+ @certs = nil
444
+ end
445
+
446
+ # Provide human-readable stringification of class state.
447
+ def inspect
448
+ "#<#{self.class} #{@address}:#{@port} open=#{@started}>"
449
+ end
450
+
451
+ # *WARNING*: This method causes a serious security hole.
452
+ # Use this method only for debugging.
453
+ #
454
+ # Set an output stream for debugging.
455
+ #
456
+ # === Example
457
+ #
458
+ # pop = Net::POP.new(addr, port)
459
+ # pop.set_debug_output $stderr
460
+ # pop.start(account, passwd) do |pop|
461
+ # ....
462
+ # end
463
+ #
464
+ def set_debug_output(arg)
465
+ @debug_output = arg
466
+ end
467
+
468
+ # The address to connect to.
469
+ attr_reader :address
470
+
471
+ # The port number to connect to.
472
+ attr_reader :port
473
+
474
+ # Seconds to wait until a connection is opened.
475
+ # If the POP3 object cannot open a connection within this time,
476
+ # it raises a TimeoutError exception.
477
+ attr_accessor :open_timeout
478
+
479
+ # Seconds to wait until reading one block (by one read(1) call).
480
+ # If the POP3 object cannot complete a read() within this time,
481
+ # it raises a TimeoutError exception.
482
+ attr_reader :read_timeout
483
+
484
+ # Set the read timeout.
485
+ def read_timeout=(sec)
486
+ @command.socket.read_timeout = sec if @command
487
+ @read_timeout = sec
488
+ end
489
+
490
+ # +true+ if the POP3 session has started.
491
+ def started?
492
+ @started
493
+ end
494
+
495
+ alias active? started? #:nodoc: obsolete
496
+
497
+ # Starts a POP3 session.
498
+ #
499
+ # When called with block, gives a POP3 object to the block and
500
+ # closes the session after block call finishes.
501
+ #
502
+ # This method raises a POPAuthenticationError if authentication fails.
503
+ def start(account, password) # :yield: pop
504
+ raise IOError, 'POP session already started' if @started
505
+ if block_given?
506
+ begin
507
+ do_start account, password
508
+ return yield(self)
509
+ ensure
510
+ do_finish
511
+ end
512
+ else
513
+ do_start account, password
514
+ return self
515
+ end
516
+ end
517
+
518
+ def do_start(account, password)
519
+ s = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
520
+ if use_ssl?
521
+ raise 'openssl library not installed' unless defined?(OpenSSL)
522
+ context = OpenSSL::SSL::SSLContext.new
523
+ context.verify_mode = @verify
524
+ if @certs
525
+ if File.file?(@certs)
526
+ context.ca_file = @certs
527
+ elsif File.directory?(@certs)
528
+ context.ca_path = @certs
529
+ else
530
+ raise ArgumentError, "certs path is not file/directory: #{@certs}"
531
+ end
532
+ end
533
+ s = OpenSSL::SSL::SSLSocket.new(s, context)
534
+ s.sync_close = true
535
+ s.connect
536
+ if context.verify_mode != OpenSSL::SSL::VERIFY_NONE
537
+ s.post_connection_check(@address)
538
+ end
539
+ end
540
+ @socket = InternetMessageIO.new(s)
541
+ logging "POP session started: #{@address}:#{@port} (#{@apop ? 'APOP' : 'POP'})"
542
+ @socket.read_timeout = @read_timeout
543
+ @socket.debug_output = @debug_output
544
+ on_connect
545
+ @command = POP3Command.new(@socket)
546
+ if apop?
547
+ @command.apop account, password
548
+ else
549
+ @command.auth account, password
550
+ end
551
+ @started = true
552
+ ensure
553
+ # Authentication failed, clean up connection.
554
+ unless @started
555
+ s.close if s and not s.closed?
556
+ @socket = nil
557
+ @command = nil
558
+ end
559
+ end
560
+ private :do_start
561
+
562
+ def on_connect
563
+ end
564
+ private :on_connect
565
+
566
+ # Finishes a POP3 session and closes TCP connection.
567
+ def finish
568
+ raise IOError, 'POP session not yet started' unless started?
569
+ do_finish
570
+ end
571
+
572
+ def do_finish
573
+ @mails = nil
574
+ @command.quit if @command
575
+ ensure
576
+ @started = false
577
+ @command = nil
578
+ @socket.close if @socket and not @socket.closed?
579
+ @socket = nil
580
+ end
581
+ private :do_finish
582
+
583
+ def command
584
+ raise IOError, 'POP session not opened yet' \
585
+ if not @socket or @socket.closed?
586
+ @command
587
+ end
588
+ private :command
589
+
590
+ #
591
+ # POP protocol wrapper
592
+ #
593
+
594
+ # Returns the number of messages on the POP server.
595
+ def n_mails
596
+ return @n_mails if @n_mails
597
+ @n_mails, @n_bytes = command().stat
598
+ @n_mails
599
+ end
600
+
601
+ # Returns the total size in bytes of all the messages on the POP server.
602
+ def n_bytes
603
+ return @n_bytes if @n_bytes
604
+ @n_mails, @n_bytes = command().stat
605
+ @n_bytes
606
+ end
607
+
608
+ # Returns an array of Net::POPMail objects, representing all the
609
+ # messages on the server. This array is renewed when the session
610
+ # restarts; otherwise, it is fetched from the server the first time
611
+ # this method is called (directly or indirectly) and cached.
612
+ #
613
+ # This method raises a POPError if an error occurs.
614
+ def mails
615
+ return @mails.dup if @mails
616
+ if n_mails() == 0
617
+ # some popd raises error for LIST on the empty mailbox.
618
+ @mails = []
619
+ return []
620
+ end
621
+
622
+ @mails = command().list.map {|num, size|
623
+ POPMail.new(num, size, self, command())
624
+ }
625
+ @mails.dup
626
+ end
627
+
628
+ # Yields each message to the passed-in block in turn.
629
+ # Equivalent to:
630
+ #
631
+ # pop3.mails.each do |popmail|
632
+ # ....
633
+ # end
634
+ #
635
+ # This method raises a POPError if an error occurs.
636
+ def each_mail(&block) # :yield: message
637
+ mails().each(&block)
638
+ end
639
+
640
+ alias each each_mail
641
+
642
+ # Deletes all messages on the server.
643
+ #
644
+ # If called with a block, yields each message in turn before deleting it.
645
+ #
646
+ # === Example
647
+ #
648
+ # n = 1
649
+ # pop.delete_all do |m|
650
+ # File.open("inbox/#{n}") do |f|
651
+ # f.write m.pop
652
+ # end
653
+ # n += 1
654
+ # end
655
+ #
656
+ # This method raises a POPError if an error occurs.
657
+ #
658
+ def delete_all # :yield: message
659
+ mails().each do |m|
660
+ yield m if block_given?
661
+ m.delete unless m.deleted?
662
+ end
663
+ end
664
+
665
+ # Resets the session. This clears all "deleted" marks from messages.
666
+ #
667
+ # This method raises a POPError if an error occurs.
668
+ def reset
669
+ command().rset
670
+ mails().each do |m|
671
+ m.instance_eval {
672
+ @deleted = false
673
+ }
674
+ end
675
+ end
676
+
677
+ def set_all_uids #:nodoc: internal use only (called from POPMail#uidl)
678
+ command().uidl.each do |num, uid|
679
+ @mails.find {|m| m.number == num }.uid = uid
680
+ end
681
+ end
682
+
683
+ def logging(msg)
684
+ @debug_output << msg + "\n" if @debug_output
685
+ end
686
+
687
+ end # class POP3
688
+
689
+ # class aliases
690
+ POP = POP3
691
+ POPSession = POP3
692
+ POP3Session = POP3
693
+
694
+ #
695
+ # This class is equivalent to POP3, except that it uses APOP authentication.
696
+ #
697
+ class APOP < POP3
698
+ # Always returns true.
699
+ def apop?
700
+ true
701
+ end
702
+ end
703
+
704
+ # class aliases
705
+ APOPSession = APOP
706
+
707
+ #
708
+ # This class represents a message which exists on the POP server.
709
+ # Instances of this class are created by the POP3 class; they should
710
+ # not be directly created by the user.
711
+ #
712
+ class POPMail
713
+
714
+ def initialize(num, len, pop, cmd) #:nodoc:
715
+ @number = num
716
+ @length = len
717
+ @pop = pop
718
+ @command = cmd
719
+ @deleted = false
720
+ @uid = nil
721
+ end
722
+
723
+ # The sequence number of the message on the server.
724
+ attr_reader :number
725
+
726
+ # The length of the message in octets.
727
+ attr_reader :length
728
+ alias size length
729
+
730
+ # Provide human-readable stringification of class state.
731
+ def inspect
732
+ "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>"
733
+ end
734
+
735
+ #
736
+ # This method fetches the message. If called with a block, the
737
+ # message is yielded to the block one chunk at a time. If called
738
+ # without a block, the message is returned as a String. The optional
739
+ # +dest+ argument will be prepended to the returned String; this
740
+ # argument is essentially obsolete.
741
+ #
742
+ # === Example without block
743
+ #
744
+ # POP3.start('pop.example.com', 110,
745
+ # 'YourAccount, 'YourPassword') do |pop|
746
+ # n = 1
747
+ # pop.mails.each do |popmail|
748
+ # File.open("inbox/#{n}", 'w') do |f|
749
+ # f.write popmail.pop
750
+ # end
751
+ # popmail.delete
752
+ # n += 1
753
+ # end
754
+ # end
755
+ #
756
+ # === Example with block
757
+ #
758
+ # POP3.start('pop.example.com', 110,
759
+ # 'YourAccount, 'YourPassword') do |pop|
760
+ # n = 1
761
+ # pop.mails.each do |popmail|
762
+ # File.open("inbox/#{n}", 'w') do |f|
763
+ # popmail.pop do |chunk| ####
764
+ # f.write chunk
765
+ # end
766
+ # end
767
+ # n += 1
768
+ # end
769
+ # end
770
+ #
771
+ # This method raises a POPError if an error occurs.
772
+ #
773
+ def pop( dest = '', &block ) # :yield: message_chunk
774
+ if block_given?
775
+ @command.retr(@number, &block)
776
+ nil
777
+ else
778
+ @command.retr(@number) do |chunk|
779
+ dest << chunk
780
+ end
781
+ dest
782
+ end
783
+ end
784
+
785
+ alias all pop #:nodoc: obsolete
786
+ alias mail pop #:nodoc: obsolete
787
+
788
+ # Fetches the message header and +lines+ lines of body.
789
+ #
790
+ # The optional +dest+ argument is obsolete.
791
+ #
792
+ # This method raises a POPError if an error occurs.
793
+ def top(lines, dest = '')
794
+ @command.top(@number, lines) do |chunk|
795
+ dest << chunk
796
+ end
797
+ dest
798
+ end
799
+
800
+ # Fetches the message header.
801
+ #
802
+ # The optional +dest+ argument is obsolete.
803
+ #
804
+ # This method raises a POPError if an error occurs.
805
+ def header(dest = '')
806
+ top(0, dest)
807
+ end
808
+
809
+ # Marks a message for deletion on the server. Deletion does not
810
+ # actually occur until the end of the session; deletion may be
811
+ # cancelled for _all_ marked messages by calling POP3#reset().
812
+ #
813
+ # This method raises a POPError if an error occurs.
814
+ #
815
+ # === Example
816
+ #
817
+ # POP3.start('pop.example.com', 110,
818
+ # 'YourAccount, 'YourPassword') do |pop|
819
+ # n = 1
820
+ # pop.mails.each do |popmail|
821
+ # File.open("inbox/#{n}", 'w') do |f|
822
+ # f.write popmail.pop
823
+ # end
824
+ # popmail.delete ####
825
+ # n += 1
826
+ # end
827
+ # end
828
+ #
829
+ def delete
830
+ @command.dele @number
831
+ @deleted = true
832
+ end
833
+
834
+ alias delete! delete #:nodoc: obsolete
835
+
836
+ # True if the mail has been deleted.
837
+ def deleted?
838
+ @deleted
839
+ end
840
+
841
+ # Returns the unique-id of the message.
842
+ # Normally the unique-id is a hash string of the message.
843
+ #
844
+ # This method raises a POPError if an error occurs.
845
+ def unique_id
846
+ return @uid if @uid
847
+ @pop.set_all_uids
848
+ @uid
849
+ end
850
+
851
+ alias uidl unique_id
852
+
853
+ def uid=(uid) #:nodoc: internal use only
854
+ @uid = uid
855
+ end
856
+
857
+ end # class POPMail
858
+
859
+
860
+ class POP3Command #:nodoc: internal use only
861
+
862
+ def initialize(sock)
863
+ @socket = sock
864
+ @error_occured = false
865
+ res = check_response(critical { recv_response() })
866
+ @apop_stamp = res.slice(/<.+>/)
867
+ end
868
+
869
+ def inspect
870
+ "#<#{self.class} socket=#{@socket}>"
871
+ end
872
+
873
+ def auth(account, password)
874
+ check_response_auth(critical {
875
+ check_response_auth(get_response('USER %s', account))
876
+ get_response('PASS %s', password)
877
+ })
878
+ end
879
+
880
+ def apop(account, password)
881
+ raise POPAuthenticationError, 'not APOP server; cannot login' \
882
+ unless @apop_stamp
883
+ check_response_auth(critical {
884
+ get_response('APOP %s %s',
885
+ account,
886
+ Digest::MD5.hexdigest(@apop_stamp + password))
887
+ })
888
+ end
889
+
890
+ def list
891
+ critical {
892
+ getok 'LIST'
893
+ list = []
894
+ @socket.each_list_item do |line|
895
+ m = /\A(\d+)[ \t]+(\d+)/.match(line) or
896
+ raise POPBadResponse, "bad response: #{line}"
897
+ list.push [m[1].to_i, m[2].to_i]
898
+ end
899
+ return list
900
+ }
901
+ end
902
+
903
+ def stat
904
+ res = check_response(critical { get_response('STAT') })
905
+ m = /\A\+OK\s+(\d+)\s+(\d+)/.match(res) or
906
+ raise POPBadResponse, "wrong response format: #{res}"
907
+ [m[1].to_i, m[2].to_i]
908
+ end
909
+
910
+ def rset
911
+ check_response(critical { get_response('RSET') })
912
+ end
913
+
914
+ def top(num, lines = 0, &block)
915
+ critical {
916
+ getok('TOP %d %d', num, lines)
917
+ @socket.each_message_chunk(&block)
918
+ }
919
+ end
920
+
921
+ def retr(num, &block)
922
+ critical {
923
+ getok('RETR %d', num)
924
+ @socket.each_message_chunk(&block)
925
+ }
926
+ end
927
+
928
+ def dele(num)
929
+ check_response(critical { get_response('DELE %d', num) })
930
+ end
931
+
932
+ def uidl(num = nil)
933
+ if num
934
+ res = check_response(critical { get_response('UIDL %d', num) })
935
+ return res.split(/ /)[1]
936
+ else
937
+ critical {
938
+ getok('UIDL')
939
+ table = {}
940
+ @socket.each_list_item do |line|
941
+ num, uid = line.split
942
+ table[num.to_i] = uid
943
+ end
944
+ return table
945
+ }
946
+ end
947
+ end
948
+
949
+ def quit
950
+ check_response(critical { get_response('QUIT') })
951
+ end
952
+
953
+ private
954
+
955
+ def getok(fmt, *fargs)
956
+ @socket.writeline sprintf(fmt, *fargs)
957
+ check_response(recv_response())
958
+ end
959
+
960
+ def get_response(fmt, *fargs)
961
+ @socket.writeline sprintf(fmt, *fargs)
962
+ recv_response()
963
+ end
964
+
965
+ def recv_response
966
+ @socket.readline
967
+ end
968
+
969
+ def check_response(res)
970
+ raise POPError, res unless /\A\+OK/i =~ res
971
+ res
972
+ end
973
+
974
+ def check_response_auth(res)
975
+ raise POPAuthenticationError, res unless /\A\+OK/i =~ res
976
+ res
977
+ end
978
+
979
+ def critical
980
+ return '+OK dummy ok response' if @error_occured
981
+ begin
982
+ return yield()
983
+ rescue Exception
984
+ @error_occured = true
985
+ raise
986
+ end
987
+ end
988
+
989
+ end # class POP3Command
990
+
991
+ end # module Net
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+
3
+ require 'lib/net/pop_ssl'
4
+ require 'lib/postman/runner'
5
+ require 'lib/postman/fetcher'
6
+ require 'lib/postman/processor'
@@ -0,0 +1,53 @@
1
+ module Postman
2
+ class Fetcher
3
+
4
+ attr_accessor :process_count
5
+
6
+ def initialize(config)
7
+ @username = config['username']
8
+ @password = config['password']
9
+ @pop_host = config['pop_host']
10
+ @pop_port = config['pop_port']
11
+ @pop_ssl = config['pop_ssl']
12
+ @working_dir = config['working_dir']
13
+ @logger = Logger.new(config['log'])
14
+
15
+ @process_count = 0
16
+
17
+ @inbox_dir = "#{@working_dir}/inbox"
18
+ FileUtils.mkdir_p("#{@inbox_dir}")
19
+
20
+ @logger.info "Fetcher starting: [username: #{@username}] [pop_host: #{@pop_host}] [working_dir: #{@working_dir}]"
21
+ end
22
+
23
+ def inbox_size
24
+ Dir.glob("#{@inbox_dir}/*").size
25
+ end
26
+
27
+ def fetch
28
+
29
+ download_count = 0
30
+
31
+ Net::POP3.enable_ssl(OpenSSL::SSL::VERIFY_NONE) if @pop_ssl
32
+ Net::POP3.start(@pop_host, @pop_port, @username, @password) do |pop|
33
+
34
+ unless pop.mails.empty?
35
+
36
+ pop.each_mail do |mail|
37
+
38
+ File.open("#{inbox_dir}/#{mail.unique_id}", 'w') do |f|
39
+ f.write mail.pop
40
+ end
41
+
42
+ download_count += 1
43
+ @process_count += 1
44
+ end
45
+
46
+ end
47
+ end
48
+
49
+ download_count
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,67 @@
1
+ require 'tmail'
2
+
3
+ module Postman
4
+
5
+ class Processor
6
+
7
+ def initialize(config)
8
+ @working_dir = config['working_dir']
9
+ @logger = Logger.new(config['log'])
10
+ @logger.info('Processor starting')
11
+ end
12
+
13
+ def process
14
+
15
+ inbox_dir = "#{@working_dir}/inbox"
16
+ archive_dir = "#{@working_dir}/archive"
17
+ problem_dir = "#{@working_dir}/problem"
18
+
19
+ FileUtils.mkdir_p("#{inbox_dir}")
20
+ FileUtils.mkdir_p("#{archive_dir}")
21
+ FileUtils.mkdir_p("#{problem_dir}")
22
+
23
+ Dir.foreach(inbox_dir) do |entry|
24
+
25
+ next if File::directory?("#{inbox_dir}/#{entry}")
26
+
27
+ @logger.debug("[mail_processor] {#{entry}} processing")
28
+
29
+ mail = File.open("#{inbox_dir}/#{entry}") do |f|
30
+ f.read
31
+ end
32
+
33
+ begin
34
+ # parse the raw email with tmail
35
+ parsed = TMail::Mail.parse(mail)
36
+
37
+ @logger.debug
38
+ @logger.debug "Subject: #{parsed.subject}"
39
+ @logger.debug "From: #{parsed.from}"
40
+ @logger.debug "To: #{parsed.to}"
41
+
42
+ rescue Exception => e
43
+ @logger.error("[mail_processor] {#{entry}} Error parsing email: #{e.message}")
44
+ FileUtils.mv("#{inbox_dir}/#{entry}", "#{problem_dir}/#{entry}")
45
+ next
46
+ end
47
+
48
+ FileUtils.mv("#{inbox_dir}/#{entry}", "#{archive_dir}/#{entry}")
49
+
50
+ if parsed.has_attachments?
51
+ @logger.debug "Has #{parsed.attachments.size} attachments"
52
+
53
+ parsed.attachments.each_with_index do |attachment, i|
54
+ @logger.debug "saving #{attachment.original_filename} [#{attachment.content_type}]"
55
+ attachment_filename = "#{archive_dir}/#{entry}__#{attachment.original_filename}"
56
+ File.open(attachment_filename, 'w') do |f|
57
+ f.write attachment.read
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,39 @@
1
+ module Postman
2
+ class Runner
3
+
4
+ def initialize(config_file)
5
+ @config = YAML.load_file(config_file)
6
+ @logger = Logger.new(@config['log'])
7
+ @running = true
8
+ end
9
+
10
+ def stop
11
+ @running = false
12
+ end
13
+
14
+ def start
15
+
16
+ ft = Thread.new do
17
+ fetcher = Postman::Fetcher.new(@config)
18
+ while @running
19
+ @logger.info "Fetcher: downloaded #{fetcher.fetch} emails | inbox: #{fetcher.inbox_size} | total processed: #{fetcher.process_count}"
20
+ sleep 30
21
+ end
22
+ @logger.info('Fetcher finished')
23
+ end
24
+
25
+ pt = Thread.new do
26
+ processor = Postman::Processor.new(@config)
27
+ while @running
28
+ @logger.info "Processor: processing..."
29
+ processor.process
30
+ sleep 30
31
+ end
32
+ @logger.info('Processor finished')
33
+ end
34
+
35
+ ft.join
36
+ pt.join
37
+ end
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: postman
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Richard Taylor
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-09 00:00:00 +00:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: daemons
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.0.10
24
+ version:
25
+ description: postman is a ruby daemon for fetching mail for an address via pop3 and then delivering it to a specified webhook
26
+ email: moomerman@gmail.com
27
+ executables:
28
+ - postman
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - README.md
35
+ - lib/net/pop_ssl.rb
36
+ - lib/postman/fetcher.rb
37
+ - lib/postman/processor.rb
38
+ - lib/postman/runner.rb
39
+ - lib/postman.rb
40
+ has_rdoc: true
41
+ homepage: http://github.com/moomerman/postman
42
+ licenses: []
43
+
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --inline-source
47
+ - --charset=UTF-8
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project: postman
65
+ rubygems_version: 1.3.5
66
+ signing_key:
67
+ specification_version: 2
68
+ summary: postman is a ruby daemon for fetching mail for an address via pop3 and then delivering it to a specified webhook
69
+ test_files: []
70
+