postman 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+