net-receiver 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bde94c119d7f2b94cb8d95a4781311477aa87fc5
4
+ data.tar.gz: b9ee22f2760943cbc37c366446f3021fda514570
5
+ SHA512:
6
+ metadata.gz: f903d9f64e4f4307ab1a447050f5bca81f14bc953810f79ac3941a8a1745205a4f808f23162e79f911d91d3a547960ea9098686780ac382bd5189fec6e562e0c
7
+ data.tar.gz: 619c8e9a7f8f993dee32714fdabe595b37dba33d7268389a81697914c8f0caf4c85b754e52702f3a648afda7f16557a86f4af4ed503fb3bcd0c25432f43b2164
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ # v1.0
2
+ This is the initial load of the gem. If you find a problem, report it to me at mjwelchphd@gmail.com, or FORK the library, fix the bug, then add a Pull Request.
data/README.md ADDED
@@ -0,0 +1,340 @@
1
+ # Net::Receiver
2
+
3
+ # EXPERIMENTAL EMAIL RECEIVER
4
+
5
+ This is an experimental email receiver which is currently (v1.0.0) working, but may be revised in ways that I can't predict at the moment. All I can guarantee is that _if_ the interface changes, it won't be major.
6
+
7
+ Currently, I'm using this as the base for an MTA written in Ruby. There's no intention of replacing Exim, Courier, or Postfix (or any other) existing MTA. The reason I'm doing this is because I need an MTA which has capabilities the standard MTAs don't offer. In other words, I need features not previously anticipated by the makers of those standard MTAs (and why would they have anticipated something I would think up in the future?)
8
+
9
+ That being said, If you use this for anything, and want me to make special changes that don't interfere with my purpose, email me at mjwelchphd@gmail.com, and I'll work with you as best I can.
10
+
11
+ # General
12
+
13
+ This gem sits on top of my net-server gem, and receives standard emails. It has only a few checks that it makes on the incoming email, leaving the specialized checks up to you. You can change it's behavior by overriding base methods, and adding your own programming; don't worry, I'll show you how. Your Ruby overrides are the same as witing a configuration file for a standard MTA.
14
+
15
+ This document describes the interface, and provides a sample program, so you can see how it works in every detail. The sample program is stored in the `example` directory.
16
+
17
+ ## What This Does
18
+
19
+ This gem received the connection from net-server, receives the email by carrying on a conversation in SMTP with the sender, and finally delivers the finished email to you. It logs stuff, if the log is enabled, and, at the moment, I have debugging in it to write to the terminal, so that I can debug more easily. Those `puts` will go away in the future.
20
+
21
+ ## TODO!
22
+
23
+ There's still stuff I need to do. A few notes in the source are prefaced with TODO! to make them easy to find. To be truthful, I'm not sure of what I may need to add, if much, in the future. I've written the receiving part of my MTA on top of this gem, so I believe that this gem is 99% complete, for what it was intended.
24
+
25
+ I also need to add a method to convert the email to the format used by the Net/* standard library classes.
26
+
27
+ However, I may also move the code to do `dig`, test for live servers, and other handy stuff into here, which will help you in your project.
28
+
29
+ # A Sample Program
30
+
31
+ Here's a sample program to demonstrate how the interface works. The complete source is in the `example` directory, so you can actually run it and see what happens. Look for the notations `#(1), #(2)` and so forth. These reference the notes below.
32
+
33
+ ```ruby
34
+ #! /usr/bin/ruby #(1)
35
+
36
+ ## For testing ...
37
+ # put in your website address in place of www.example.com
38
+ # swaks -s www.example.com:2000 -t coco@smith.com -f jamie@glock.com --ehlo example.com
39
+ # swaks -tls -s www.example.com:2000 -t coco@smith.com -f jamie@glock.com --ehlo example.com
40
+
41
+ require 'net/receiver' #(2)
42
+ require 'logger'
43
+ require 'sequel'
44
+ require 'yaml'
45
+ require 'pretty_inspect'
46
+
47
+ class Receiver < Net::ReceiverCore #(3)
48
+
49
+ def password(username) #(4)
50
+ # DB[:mailboxes].where(:email=>username).first[:passwd]
51
+ end
52
+
53
+ def received(mail) #(5)
54
+ puts "--> *99* #{mail.pretty_inspect}"
55
+ end
56
+
57
+ end
58
+
59
+ # Open the log #(6)
60
+ LOG = Logger::new('log/test3.log', 'daily')
61
+ LOG.formatter = proc do |severity, datetime, progname, msg|
62
+ pname = if progname then '('+progname+') ' else nil end
63
+ "#{datetime.strftime("%Y-%m-%d %H:%M:%S")} [#{severity}] #{pname}#{msg}\n"
64
+ end
65
+
66
+ # Open the database #(7)
67
+ if ['dev','live'].index(ENV['MODE']).nil?
68
+ msg = "Environmental variable MODE not set properly--must be dev or live"
69
+ LOG.fatal(msg)
70
+ puts msg
71
+ exit(1)
72
+ end
73
+ host = YAML.load_file("./database.yml")[ENV['MODE']]
74
+ DB = Sequel.connect(host)
75
+ LOG.info("Database \"#{host['database']}\" opened")
76
+
77
+ # Start the server #(8)
78
+ options = {
79
+ :server_name=>"www.example.com",
80
+ :private_key=>"server.key",
81
+ :certificate=>"server.crt",
82
+ :listening_ports=>['2000','2001'],
83
+ :ehlo_validation_check=>true
84
+ }
85
+ Net::Server.new(options).start
86
+ ```
87
+ Here's the breakdown (see references like #(1), etc.):
88
+
89
+ 1. This line makes `test3` self executing.
90
+ 2. There are requires for `net/receiver`. It will require `net/server`, so you don't have to do that. It requires `logger` to demonstrate how to open a logger file; `sequel` because I use Sequel in my programming; `yaml` for reading the database 'yaml' file in the project (you'll have to change all this database stuff to your liking); and `pretty_inspect` which makes it easier to see what's coming out (you can install the gem for that).
91
+ 3. Define your receiver like this. The base in `net-receiver` is called `ReceiverCore` in order that you can derive class `Receiver` from it.
92
+ 4. In order to use authorization (only PLAIN supported at this time), you need this code to provide the password for a user.
93
+ 5. The received email is delivered to the `received` method. See the example of what gets delivered below.
94
+ 6. This is how to open a log file. The name LOG is used because it is traditional.
95
+ 7. This is how you open a Sequel/MySQL database. You may remove this code if you don't use Sequel to read passwords from the database.
96
+ 8. The last part is the server start code. Look at "https://github.com/mjwelchphd/net-server" documentation for more information.
97
+
98
+ # How It Works
99
+
100
+ The class ReceiverCode uses a table named Patterns to guide the receiving process. For each line of the table, the number on the left is a 'level', i.e., STARTTLS (level 2) cannot come before EHLO or HELO (level 1). The pattern describing the value is next: the input value on the command line must match this pattern. The method which handles the command is last.
101
+
102
+ As each line is read on the communications channel, it is matched up with this table, and if all is well, the method is called to deal with it.
103
+
104
+ The `send_text` and `recv_text` methods are complex because they have to handle conditions like the client slamming the communications channel shut, and so forth.
105
+
106
+ The method `psych_value` is used with MAIL FROM and RCPT TO commands to validate and investigate the email addresses given.
107
+
108
+ 1. It checks that there is a legitimate address, with or with a preceeding name.
109
+ 2. It breaks the address up into a local-part and a domain.
110
+ 3. If the option is selected, it tests for the legal usage of dots (".") in the name, and legal characters which net-receiver defines as
111
+ - uppercase and lowercase English letters (a-z, A-Z)
112
+ - digits 0 to 9
113
+ - characters ! # $ % & ' * + - / = ? ^ _ ` { | } ~
114
+ - dots, which must not be first or last character, and must not appear two or more times consecutively
115
+ 4. If the option is selected, it does a Dig MX lookup, followed by a Dig A (IP) lookup if the MX was successful. This is helpful to determine if the sender's domain is legitimate.
116
+
117
+ The main method is the `receive` method (which is called by net-server when a connection is requested). Receive uses the aforementioned table to read the commands and process them. It also allocates an 'item of mail' structure to put it's findings in.
118
+
119
+ If `@mail[:prohibited] gets set to `true`, the loop will terminate and the connection will be closed. This is mainly for shutting down spammers who make large numbers of calls in a short period of time (DDOS attacks).
120
+
121
+ Any method can `raise Quit` in order to terminate the reception also.
122
+
123
+ When the main loop terminates, for whatever reason, the email will be delivered to your `received` method. When your `received` method terminates, the process is cleaned up and terminated.
124
+
125
+ Methods that begin with `do_` are the methods with do any generalized processing of the commands. Typically, they create a key in the item-of-mail and all the data for that command is stored in that hash. There are very few validations of the incoming data because that is the job of your method overrides (to be described below).
126
+
127
+ Methods that are named the same as the commands, i.e., `connect`, `ehlo`, `quit`, `auth`, `expn`, `help`, `noop`, `rset`, `vfry`, `mail_from`, `rcpt_to`, and `data` deliver the default response back to the `receive` method.
128
+
129
+ You can change their behavior by overriding them like this example:
130
+
131
+ ```ruby
132
+ class Receiver < Net::ReceiverCore
133
+
134
+ def mail_from(from)
135
+ return "556 5.7.27 Traffic on port #{@options[:submission_port]} must be authenticated" \
136
+ if !@mail[:authenticated]
137
+ return "556 5.7.27 Traffic on port #{@options[:submission_port]} must be encrypted" \
138
+ if !@mail[:encrypted]
139
+ super
140
+ end
141
+
142
+ end
143
+ ```
144
+
145
+ In this example, `def mail_from` overrides the method of the same name in ReceiverCore. It tests :authenticated and :encrypted, and if there is an error, it returns the error message; if not, it performs `super` which returns the default message. Don't forget to call `super`.
146
+
147
+ # Start Options for Server
148
+
149
+ Option | Default | Description
150
+ --- | --- | ---
151
+ :server_name | "example.com" | This name is only used in error messages.
152
+ :listening_ports | ["25","486","587"] | An array of one or more ports to listen on.
153
+ :private_key | Internal key | The key for encrypting/decrypting the data when in TLS mode.
154
+ :certificate | Internal self-signed certificate | The certificate for encrypting/decrypting the data when in TLS mode. This may be your own self-signed certificate, or one you purchase from a Certificate Authority, or you can become a Certificate Authority and sign your own.
155
+ :user_name | nil | This name is the user name to which each process will be switched after it is created. If it is nil, the ownership of the process will not be changed after creation. If you are using a port less than 1024, you must start the server as root, and the user name and group name of the process _must be_ specified.
156
+ :group_name | nil | This name is the group name to which each process will be switched after it is created.
157
+ :working_directory | the current path | The location of the program running the server.
158
+ :pid_file | "pid" | The PID of the server will be stored in this file.
159
+ :daemon | false | If this option is true, the server will be started as a daemon.
160
+
161
+ # Start Options for Receiver
162
+
163
+ Option | Default | Description
164
+ --- | --- | ---
165
+ :ehlo_validation_check | false | This makes `receiver` test the domain name given on the EHLO or HELO line.
166
+ :sender_character_check | true | This makes `receiver` test for legal characters on the MAIL FROM address.
167
+ :recipient_character_check | false | This makes `receiver`test for legal characters on the RCPT TO address.
168
+ :sender_mx_check | true | Tries to obtain the MX name and IP from the DNS for MAIL FROM.
169
+ :recipient_mx_check | false | Tries to obtain the MX name and IP from the DNS for RCPT TO.
170
+ :max_failed_msgs_per_period | 3 | I use this to say, "after 3 failed attempts, lock out the sender for s short period of time (10 minutes in my case)."
171
+
172
+ I may add more defaults to this list in the future, but I'll try to make them generalized, so they fit anyone's need.
173
+
174
+ # The Structure That Comes Out
175
+
176
+ Here is a sample structure for an authenticated email.
177
+
178
+ ```text
179
+ {
180
+ :local_port=>"2001",
181
+ :local_hostname=>"mail.example.com",
182
+ :remote_port=>"38436",
183
+ :remote_hostname=>"cpe-107-185-187-182.socal.res.rr.com",
184
+ :remote_ip=>"::ffff:107.185.187.182",
185
+ :id=>"ODZM0W-PRPAYD-49",
186
+ :time=>"2016-09-24 02:38:56 +0000",
187
+ :accepted=>true,
188
+ :prohibited=>false,
189
+ :encrypted=>true,
190
+ :authenticated=>"admin@example.com",
191
+ :connect=>{
192
+ :value=>"::ffff:107.185.187.182",
193
+ :domain=>nil
194
+ },
195
+ :ehlo=>{
196
+ :value=>"mail.example.com",
197
+ :rip=>"23.253.107.107",
198
+ :fip=>"23.253.107.107",
199
+ :domain=>"mail.example.com"
200
+ },
201
+ :mailfrom=>{
202
+ :accepted=>true,
203
+ :value=>"<admin@example.com>",
204
+ :name=>"",
205
+ :url=>"admin@example.com",
206
+ :local_part=>"admin",
207
+ :domain=>"example.com",
208
+ :bad_characters=>false,
209
+ :wrong_dot_usage=>false,
210
+ :ip=>"23.253.107.107",
211
+ :mxs=>[
212
+ "mail.example.com"
213
+ ],
214
+ :ips=>[
215
+ "23.253.107.107"
216
+ ]
217
+ },
218
+ :rcptto=>[
219
+ {
220
+ :accepted=>true,
221
+ :value=>"<coco@example.com>",
222
+ :name=>"",
223
+ :url=>"coco@example.com",
224
+ :local_part=>"coco",
225
+ :domain=>"example.com"
226
+ }
227
+ ],
228
+ :data=>{
229
+ :accepted=>true,
230
+ :value=>"",
231
+ :text=>[
232
+ "Date: Fri, 23 Sep 2016 19:38:55 -0700",
233
+ "To: coco@example.com",
234
+ "From: admin@example.com",
235
+ "Subject: test Fri, 23 Sep 2016 19:38:55 -0700",
236
+ "X-Mailer: swaks v20130209.0 jetmore.org/john/code/swaks/",
237
+ "",
238
+ "This is a test mailing",
239
+ "",
240
+ "."
241
+ ],
242
+ :headers=>{
243
+ :date=>"Fri, 23 Sep 2016 19:38:55 -0700",
244
+ :to=>"coco@example.com",
245
+ :from=>"admin@example.com",
246
+ :subject=>"test Fri, 23 Sep 2016 19:38:55 -0700",
247
+ :x_mailer=>"swaks v20130209.0 jetmore.org/john/code/swaks/"
248
+ }
249
+ }
250
+ }
251
+ ```
252
+
253
+ ## Format of Delivered Mail
254
+
255
+ ### Global Values
256
+
257
+ |Symbol |Description |
258
+ |:--- |:--- |
259
+ | :local_port | This is the port on your machine that the user connected to. |
260
+ | :local_hostname | This is the `hostname` of your machine. |
261
+ | :remote_port | This is the port on the remote machine that originated the connection. |
262
+ | :remote_hostname | This is the `hostname` of the remote machine. |
263
+ | :remote_ip | This is the IP of the remote machine. |
264
+ | :id | This is the Message ID generated by `Receiver`. Note that there should alread be header `Message-ID` in the email, but if not, this one can be inserted. |
265
+ | :time | This is the time the conncetion was made. |
266
+ | :accepted | This is a true/false which indicates whether the email should be accepted. |
267
+ | :prohibited | If you set this flag, `Receiver` will treat the email as spam, and the sender IP as a spammer. |
268
+ | :encrypted | This true/false indicates whether or not a STARTTLS was completed. |
269
+ | :authenticated | This value is nil or the email address of the authenticated entity. |
270
+
271
+ ### CONNECT Values
272
+
273
+ |Symbol |Description |
274
+ |:--- |:--- |
275
+ | :value | This is the remote IP (taken from the value above). |
276
+ | :domain | If a domain can be discovered for the remote IP, it will be here. |
277
+
278
+ ### EHLO Values
279
+
280
+ |Symbol |Description |
281
+ |:--- |:--- |
282
+ | :value | This is the raw data supplied on the EHLO line. |
283
+ | :rip | This is the reverse IP, if any, obtained by looking up the value. |
284
+ | :fip | The reverse IP is used to get the MX, which is then looked up to get this forward IP. |
285
+ | :domain | This is the MX value obtained from looking up the reverse IP. |
286
+
287
+ ### MAIL FROM Values
288
+
289
+ |Symbol |Description |
290
+ |:--- |:--- |
291
+ | :accepted | This true/false value indicates if the MAIL FROM value appears to be acceptable. |
292
+ | :value | This is the raw data presented on the MAIL FROM line. |
293
+ | :name | If a name preceeded the email address, it is put here. |
294
+ | :url | This is the "pure" email address in the MAIL FROM statement. |
295
+ | :local_part | This is the "local-part" of the URL above. |
296
+ | :domain | This is the domain of the URL above. |
297
+ | :bad_characters | This true/false tells whether bad characters were found in the local-part. |
298
+ | :wrong_dot_usage | This true/false tells whether dots were mis-used in the local-part. |
299
+ | :ip | This is the IP from looking up the domain. |
300
+ | :mxs | This is a list of one or more mail servers for this domain. |
301
+ | :ips | This is a list of IPs obtained by looking up the MXs above. |
302
+
303
+ ### RCPT TO Values (a list)
304
+
305
+ |Symbol |Description |
306
+ |:--- |:--- |
307
+ | :accepted | This true/false value indicates if the RCPT TO value appears to be acceptable. |
308
+ | :value | This is the raw data presented on the RCPT TO line. |
309
+ | :name | If a name preceeded the email address, it is put here. |
310
+ | :url | This is the "pure" email address in the RCPT TO statement. |
311
+ | :local_part | This is the "local-part" of the URL above. |
312
+ | :domain | This is the domain of the URL above. |
313
+
314
+ ### DATA Values (including the email proper)
315
+
316
+ |Symbol |Description |
317
+ |:--- |:--- |
318
+ | :accepted | This tru/false value indicates whether `Receiver` accepted the email from the sender. |
319
+ | :value | This should be an empty string. |
320
+ | :text | __This is the body of the email proper.__ It's organized as an array of lines with the CRLFs stripped off the ends. |
321
+
322
+ **NOTE! If [:data][:accepted] is *true*, you have taken full responsibility for the email. You must either deliver it, forward it, or bounce it.**
323
+
324
+ #### Headers (broken out to make access easier)
325
+
326
+ Here's an example:
327
+
328
+ |Key |Value |
329
+ |:--- |:--- |
330
+ | :date | "Fri, 23 Sep 2016 19:38:55 -0700", |
331
+ | :to | "coco@example.com", |
332
+ | :from | "admin@example.com", |
333
+ | :subject | "test Fri, 23 Sep 2016 19:38:55 -0700", |
334
+ | :x_mailer | "swaks v20130209.0 jetmore.org/john/code/swaks/" |
335
+
336
+
337
+ The headers are put into a hash like this so that you may easily locate them, or test to see if they exist or not. Modifying this Hash *does not* modify the actual email.
338
+
339
+
340
+ FIN
@@ -0,0 +1,182 @@
1
+ # = net/extended_classes.rb
2
+ #
3
+ # Copyright (c) 2016 Michael J. Welch, Ph.D.
4
+ #
5
+ # Written and maintained by Michael J. Welch, Ph.D. <mjwelchphd@gmail.com>
6
+ #
7
+ # This work is not derived from any other author. It is original software.
8
+ #
9
+ # Documented by Michael J. Welch, Ph.D. <mjwelchphd@gmail.com>
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
+ #
14
+ # See the README.md for documentation.
15
+
16
+ require 'resolv'
17
+ require 'base64'
18
+ require 'unix_crypt'
19
+
20
+ LiveServerTestTimeout = 15
21
+
22
+ class QueryError < StandardError; end
23
+
24
+ class Object
25
+ def deepclone
26
+ case
27
+ when self.class==Hash
28
+ hash = {}
29
+ self.each { |k,v| hash[k] = v.deepclone }
30
+ hash
31
+ when self.class==Array
32
+ array = []
33
+ self.each { |v| array << v.deepclone }
34
+ array
35
+ else
36
+ if defined?(self.class.new)
37
+ self.class.new(self)
38
+ else
39
+ self
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ class NilClass
46
+ # these defs allow for the case where something wasn't found to
47
+ # give a nil response rather than crashing--for example:
48
+ # mx = "example.com" # => nil (because example.com has no MX record)
49
+ # ip = mx.dig_a # => nil, without crashing
50
+ # otherwise, it would be necessary to write:
51
+ # mx = "example.com" # => nil (because example.com has no MX record)
52
+ # ip = if mx then ip = mx.dig_a else ip = nil end
53
+ def dig_a; nil; end
54
+ def dig_aaaa; nil; end
55
+ def dig_mx; nil; end
56
+ def dig_dk; nil; end
57
+ def dig_ptr; nil; end
58
+ def mta_live?(port); nil; end
59
+ def validate_plain; return "", false; end
60
+ # the [] method allows for x[:a][:b]... type references where
61
+ # x[:a] --> nil to return another nil rather than crash
62
+ def [](what)
63
+ nil
64
+ end
65
+ end
66
+
67
+ class String
68
+ # returns list of IPV4 addresses, or nil
69
+ # (there should only be one IPV4 address)
70
+ def dig_a
71
+ Resolv::DNS.open do |dns|
72
+ txts = dns.getresources(self,Resolv::DNS::Resource::IN::A).collect { |r| r.address.to_s }
73
+ if txts.empty? then nil else txts[0] end
74
+ end
75
+ end
76
+
77
+ # returns list of IPV6 addresses, or nil
78
+ # (there should only be one IPV6 address)
79
+ def dig_aaaa
80
+ Resolv::DNS.open do |dns|
81
+ txts = dns.getresources(self,Resolv::DNS::Resource::IN::AAAA).collect { |r| r.address.to_s.downcase }
82
+ if txts.empty? then nil else txts[0] end
83
+ end
84
+ end
85
+
86
+ # returns list of MX names, or nil
87
+ # (there may be multiple MX names for a domain)
88
+ def dig_mx
89
+ Resolv::DNS.open do |dns|
90
+ txts = dns.getresources(self,Resolv::DNS::Resource::IN::MX).collect { |r| r.exchange.to_s }
91
+ if txts.empty? then nil else txts end
92
+ end
93
+ end
94
+
95
+ # returns a publibdomainkey, or nil
96
+ # (there should only be one DKIM public key)
97
+ def dig_dk
98
+ Resolv::DNS.open do |dns|
99
+ txts = dns.getresources(self,Resolv::DNS::Resource::IN::TXT).collect { |r| r.strings }
100
+ if txts.empty? then nil else txts[0][0] end
101
+ end
102
+ end
103
+
104
+ # returns a reverse DNS hostname or nil
105
+ def dig_ptr
106
+ begin
107
+ Resolv.new.getname(self.downcase)
108
+ rescue Resolv::ResolvError
109
+ nil
110
+ end
111
+ end
112
+
113
+ # returns true if the IP is blacklisted; otherwise false
114
+ # examples:
115
+ # barracuda = 'b.barracudacentral.org'.blacklisted?(ip)
116
+ # spamhaus = 'zen.spamhaus.org'.blacklisted?(ip)
117
+ def blacklisted?(dx)
118
+ domain = dx.split('.').reverse.join('.')+"."+self
119
+ a = []
120
+ Resolv::DNS.open do |dns|
121
+ begin
122
+ a = dns.getresources(domain, Resolv::DNS::Resource::IN::A)
123
+ rescue Resolv::NXDomainError
124
+ a=[]
125
+ end
126
+ end
127
+ if a.size>0 then true else false end
128
+ end
129
+
130
+ # returns a UTF-8 encoded string -- be carefule using this with email:
131
+ # email has to be received and transported with NO changes, except the
132
+ # addition of extra headers at the beginning (before any DKIM headers)
133
+ def utf8
134
+ self.encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace, :replace => '?')
135
+ end
136
+
137
+ # opens a socket to the IP/port to see if there is an SMTP server
138
+ # there - returns "250 ..." if the server is there, or
139
+ # times out in 5 seconds to prevent hanging the process
140
+ def mta_live?(port)
141
+ tcp_socket = nil
142
+ welcome = nil
143
+ begin
144
+ Timeout.timeout(LiveServerTestTimeout) do
145
+ begin
146
+ tcp_socket = TCPSocket.open(self,port)
147
+ rescue Errno::ECONNREFUSED => e
148
+ return "421 Service not available (port closed)"
149
+ end
150
+ begin
151
+ welcome = tcp_socket.gets
152
+ return welcome if welcome[1]!='2'
153
+ tcp_socket.write("QUIT\r\n")
154
+ line = tcp_socket.gets
155
+ return line if line[1]!='2'
156
+ ensure
157
+ tcp_socket.close if tcp_socket
158
+ end
159
+ end
160
+ return "250 #{welcome.chomp[4..-1]}"
161
+ rescue SocketError => e
162
+ return "421 Service not available (#{e.to_s})"
163
+ rescue Timeout::Error => e
164
+ return "421 Service not available (#{e.to_s})"
165
+ end
166
+ end
167
+
168
+ # this validates a password with the base64 plaintext in an AUTH command
169
+ # encoded -> AGNvY29AY3phcm1haWwuY29tAG15LXBhc3N3b3Jk => ["coco@example.com", "my-password"]
170
+ # "my-password" --> {CRYPT}IwYH/ZXeR8vUM
171
+ # "AGNvY29AY3phcm1haWwuY29tAG15LXBhc3N3b3Jk".validate_plain { "{CRYPT}IwYH/ZXeR8vUM" } => "coco@example.com", true
172
+ # "AGNvY29AY3phcm1haWwuY29tAHh4LXBhc3N3b3Jk".validate_plain { "{CRYPT}IwYH/ZXeR8vUM" } => "coco@example.com", false
173
+ def validate_plain
174
+ # decode and split up the username and password)
175
+ username, password = Base64::decode64(self).split("\x00")[1..-1]
176
+ return "", false if username.nil? || password.nil?
177
+ passwd_hash = yield(username) # get the hash
178
+ return "", false if passwd_hash.nil?
179
+ m = passwd_hash.match(/^{(.*)}(.*)$/)
180
+ return username, UnixCrypt.valid?(password, m[2])
181
+ end
182
+ end
@@ -0,0 +1,77 @@
1
+ # = net/item_of_mail.rb
2
+ #
3
+ # Copyright (c) 2016 Michael J. Welch, Ph.D.
4
+ #
5
+ # Written and maintained by Michael J. Welch, Ph.D. <mjwelchphd@gmail.com>
6
+ #
7
+ # This work is not derived from any other author. It is original software.
8
+ #
9
+ # Documented by Michael J. Welch, Ph.D. <mjwelchphd@gmail.com>
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
+ #
14
+ # See the README.md for documentation.
15
+
16
+ require 'sequel'
17
+
18
+ module Net
19
+ class ItemOfMail < Hash
20
+ def initialize(local_port, local_hostname, remote_port, remote_hostname, remote_ip)
21
+ self[:local_port] = local_port
22
+ self[:local_hostname] = local_hostname
23
+ self[:remote_port] = remote_port
24
+ self[:remote_hostname] = remote_hostname
25
+ self[:remote_ip] = remote_ip
26
+
27
+ new_id = []
28
+ new_id[0] = Time.now.tv_sec.to_s(36).upcase
29
+ new_id[1] = ("000000"+(2176782336*rand).to_i.to_s(36))[-6..-1].upcase
30
+ new_id[2] = ("00"+(Time.now.usec/1000).to_i.to_s(36))[-2..-1].upcase
31
+ self[:id] = new_id.join("-")
32
+
33
+ self[:time] = Time.now.strftime("%Y-%m-%d %H:%M:%S %z")
34
+ end
35
+
36
+ def parse_headers
37
+ self[:data][:headers] = {}
38
+ header = ""
39
+ self[:data][:text].each do |line|
40
+ case
41
+ when line.nil?
42
+ break
43
+ when line =~ /^[ \t]/
44
+ header << String::new(line)
45
+ when line.empty?
46
+ break
47
+ when !header.empty?
48
+ keyword, value = header.split(":", 2)
49
+ self[:data][:headers][keyword.downcase.gsub("-","_").to_sym] = value.strip
50
+ header = String::new(line)
51
+ else
52
+ header = String::new(line)
53
+ end
54
+ end
55
+ if !header.empty?
56
+ keyword, value = header.split(":", 2)
57
+ self[:data][:headers][keyword.downcase.gsub("-","_").to_sym] = if !value.nil? then value.strip else "" end
58
+ end
59
+ end
60
+
61
+ def spf_check(scope,identity,ip,ehlo)
62
+ spf_server = SPF::Server.new
63
+ begin
64
+ request = SPF::Request.new(
65
+ versions: [1, 2],
66
+ scope: scope,
67
+ identity: identity,
68
+ ip_address: ip,
69
+ helo_identity: ehlo)
70
+ spf_server.process(request).code
71
+ rescue SPF::OptionRequiredError => e
72
+ @log.info("%06d"%Process::pid) {"SPF check failed: #{e.to_s}"}
73
+ :fail
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,522 @@
1
+ # = net/receiver.rb
2
+ #
3
+ # Copyright (c) 2016 Michael J. Welch, Ph.D.
4
+ #
5
+ # Written and maintained by Michael J. Welch, Ph.D. <mjwelchphd@gmail.com>
6
+ #
7
+ # This work is not derived from any other author. It is original software.
8
+ #
9
+ # Documented by Michael J. Welch, Ph.D. <mjwelchphd@gmail.com>
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
+ #
14
+ # See the README.md for documentation.
15
+
16
+ require 'net/server'
17
+ require 'net/item_of_mail'
18
+ require 'net/extended_classes'
19
+ require 'pdkim'
20
+
21
+ class Quit < Exception; end
22
+
23
+ module Net
24
+
25
+ # == An Email receiver
26
+ class ReceiverCore
27
+ CRLF = "\r\n"
28
+ Patterns = [
29
+ [0, "[ /t]*QUIT[ /t]*", :do_quit],
30
+ [0, "[ /t]*SLAM[ /t]*", :do_slam],
31
+ [0, "[ /t]*TIMEOUT[ /t]*", :do_timeout],
32
+ [1, "[ /t]*AUTH[ /t]*(.+)", :do_auth],
33
+ [1, "[ /t]*EHLO(.*)", :do_ehlo],
34
+ [1, "[ /t]*EXPN[ /t]*", :do_expn],
35
+ [1, "[ /t]*HELO[ /t]+(.*)", :do_ehlo],
36
+ [1, "[ /t]*HELP[ /t]*", :do_help],
37
+ [1, "[ /t]*NOOP[ /t]*", :do_noop],
38
+ [1, "[ /t]*RSET[ /t]*", :do_rset],
39
+ [1, "[ /t]*TIMEOUT[ /t]*", :do_timeout],
40
+ [1, "[ /t]*VFRY[ /t]*", :do_vfry],
41
+ [2, "[ /t]*STARTTLS[ /t]*", :do_starttls],
42
+ [2, "[ /t]*MAIL FROM[ /t]*:[ \t]*(.+)", :do_mail_from],
43
+ [3, "[ /t]*RCPT TO[ /t]*:[ \t]*(.+)", :do_rcpt_to],
44
+ [4, "[ /t]*DATA[ /t]*", :do_data]
45
+ ]
46
+ Kind = {:mailfrom=>"MAIL FROM", :rcptto=>"RCPT TO"}
47
+ ReceiverTimeout = 30
48
+ LogConversation = true
49
+ Unexpectedly = "; probably caused by the client closing the connection unexpectedly"
50
+
51
+ include PDKIM
52
+
53
+ DkimOutcomes = {
54
+ PDKIM_VERIFY_NONE=>"PDKIM_VERIFY_NONE",
55
+ PDKIM_VERIFY_INVALID=>"PDKIM_VERIFY_INVALID",
56
+ PDKIM_VERIFY_FAIL=>"PDKIM_VERIFY_FAIL",
57
+ PDKIM_VERIFY_PASS=>"PDKIM_VERIFY_PASS",
58
+ PDKIM_FAIL=>"PDKIM_FAIL",
59
+ PDKIM_ERR_OOM=>"PDKIM_ERR_OOM",
60
+ PDKIM_ERR_RSA_PRIVKEY=>"PDKIM_ERR_RSA_PRIVKEY",
61
+ PDKIM_ERR_RSA_SIGNING=>"PDKIM_ERR_RSA_SIGNING",
62
+ PDKIM_ERR_LONG_LINE=>"PDKIM_ERR_LONG_LINE",
63
+ PDKIM_ERR_BUFFER_TOO_SMALL=>"PDKIM_ERR_BUFFER_TOO_SMALL"
64
+ }
65
+
66
+ def initialize(connection, options)
67
+ @connection = connection
68
+ @option_list = [[:ehlo_validation_check, false], [:sender_character_check, true],
69
+ [:recipient_character_check, false], [:sender_mx_check, true],
70
+ [:recipient_mx_check, false],[:max_failed_msgs_per_period,3]]
71
+ @options = options
72
+ @option_list.each do |key,value|
73
+ @options[key] = value if !options.has_key?(key)
74
+ end
75
+ @enc_ind = '-'
76
+ end
77
+
78
+ #-------------------------------------------------------#
79
+ #--- Send text to the client ---------------------------#
80
+ #-------------------------------------------------------#
81
+ def log_msg_if_level_5(msg)
82
+ if msg[0]=='5'
83
+ m = msg.match(/^([0-9]{3} [0-9]\.[0-9]\.[0-9] )/)
84
+ start = if !m then 0 else m[1].size end
85
+ LOG.error("%06d"%Process::pid) {msg[start..-1]}
86
+ end
87
+ end
88
+
89
+ def write_text(text, echo)
90
+ puts "<#{@enc_ind} #{text.inspect}"
91
+ @connection.write(text)
92
+ @connection.write(CRLF)
93
+ @has_level_5_warnings = true if text[0]=='5'
94
+ LOG.info("%06d"%Process::pid) {"<#{@enc_ind} #{text}"} if echo && LogConversation
95
+ log_msg_if_level_5(text)
96
+ end
97
+
98
+ def send_text(text,echo=true)
99
+ begin
100
+ case
101
+ when text.nil?
102
+ # do nothing
103
+ when text.class==Array
104
+ text.each { |line| write_text(line, echo) }
105
+ when text.class==String
106
+ write_text(text, echo)
107
+ end
108
+ rescue Errno::EPIPE => e
109
+ LOG.error("%06d"%Process::pid) {"#{e.to_s}#{Unexpectedly}"}
110
+ raise Quit
111
+ rescue Errno::EIO => e
112
+ LOG.error("%06d"%Process::pid) {"#{e.to_s}#{Unexpectedly}"}
113
+ raise Quit
114
+ end
115
+ end
116
+
117
+ #-------------------------------------------------------#
118
+ #--- Receive text from the client ----------------------#
119
+ #-------------------------------------------------------#
120
+ def recv_text(echo=true)
121
+ begin
122
+ Timeout.timeout(ReceiverTimeout) do
123
+ begin
124
+ temp = @connection.gets
125
+ if temp.nil?
126
+ LOG.warn("%06d"%Process::pid) {"The client abruptly closed the connection"}
127
+ text = "QUIT"
128
+ else
129
+ text = temp.chomp
130
+ end
131
+ rescue Errno::ECONNRESET => e
132
+ LOG.warn("%06d"%Process::pid) {"The client slammed the connection shut"}
133
+ text = "SLAM"
134
+ end
135
+ LOG.info("%06d"%Process::pid) {" #{@enc_ind}> #{text}"} if echo && LogConversation
136
+ puts " #{@enc_ind}> #{text.inspect}"
137
+ return text
138
+ end
139
+ rescue Errno::EIO => e
140
+ LOG.error("%06d"%Process::pid) {"#{e.to_s}"}
141
+ raise Quit
142
+ rescue Timeout::Error => e
143
+ puts " #{@enc_ind}> \"TIMEOUT\""
144
+ return "TIMEOUT"
145
+ end
146
+ puts " #{@enc_ind}> *669* Investigate why this got here"
147
+ end
148
+
149
+ #-------------------------------------------------------#
150
+ #--- Parse the email address and investigate it --------#
151
+ #-------------------------------------------------------#
152
+ def psych_value(kind, part, value)
153
+ # the value gets set in both MAIL FROM and RCPT TO
154
+ part[:value] = value
155
+
156
+ # there MUST be a sender/recipient address
157
+ return "501 5.1.7 '#{part[:value]}' No proper address (<...>) on the #{Kind[kind]} line" \
158
+ if (m = value.match(/^(.*)<(.+@.+\..+)>$/)).nil?
159
+
160
+ # break up the address
161
+ part[:name] = m[1].strip
162
+ part[:url] = url = m[2].strip
163
+
164
+ # parse out the local-part and domain
165
+ local_part, domain = url.split("@")
166
+ part[:local_part] = local_part
167
+ part[:domain] = domain
168
+
169
+ if ((kind==:mailfrom) && (@options[:sender_character_check])) \
170
+ || ((kind==:rcptto) && (@options[:recipient_character_check]))
171
+ # check the local part:
172
+ # uppercase and lowercase English letters (a-z, A-Z)
173
+ # digits 0 to 9
174
+ # characters ! # $ % & ' * + - / = ? ^ _ ` { | } ~
175
+ part[:bad_characters] = local_part.match(/^[a-zA-Z0-9\!\#\$%&'*+-\/?^_`{|}~]+$/).nil?
176
+ # check character . must not be first or last character,
177
+ # and must not appear two or more times consecutively
178
+ part[:wrong_dot_usage] = !(local_part[0]=='.' || local_part[-1]=='.' || local_part.index('..')).nil?
179
+ end
180
+
181
+ # skip this if not needed
182
+ if ((kind==:mailfrom) && (@options[:sender_mx_check])) \
183
+ || ((kind==:rcptto) && (@options[:recipient_mx_check]))
184
+ # get the ip for this domain
185
+ part[:ip] = ip = domain.dig_a
186
+
187
+ # get the mx record(s)
188
+ part[:mxs] = mxs = domain.dig_mx
189
+
190
+ # get the mx's ip records
191
+ if mxs
192
+ part[:ips] = ips = []
193
+ mxs.each { |mx| ips << mx.dig_a }
194
+ end
195
+ end
196
+
197
+ # email address investigation completed
198
+ return nil
199
+ end
200
+
201
+ #-------------------------------------------------------#
202
+ #--- Receive the connection ----------------------------#
203
+ #-------------------------------------------------------#
204
+ def receive(local_port, local_hostname, remote_port, remote_hostname, remote_ip)
205
+ # Start a hash to collect the information gathered from the receive process
206
+ @mail = Net::ItemOfMail::new(local_port, local_hostname, remote_port, remote_hostname, remote_ip)
207
+ @mail[:accepted] = false
208
+ @mail[:prohibited] = false
209
+
210
+ # start the main receiving process here
211
+ @done = false
212
+ @mail[:encrypted] = false
213
+ @mail[:authenticated] = false
214
+ send_text(do_connect(remote_ip))
215
+ @level = 1
216
+ @has_level_5_warnings = false
217
+
218
+ begin
219
+ break if @done
220
+ text = recv_text
221
+ unrecognized = true
222
+ Patterns.each do |pattern|
223
+ break if pattern[0]>@level
224
+ m = text.match(/^#{pattern[1]}$/i)
225
+ if m
226
+ case
227
+ when pattern[2]==:do_quit
228
+ send_text(do_quit(m[1]))
229
+ when pattern[2]==:do_slam
230
+ send_text(do_slam(m[1]))
231
+ when @mail[:prohibited]
232
+ send_text("450 4.7.1 Sender IP #{@mail[:remote_ip]} is temporarily prohibited from sending")
233
+ when pattern[0]>@level
234
+ send_text("503 5.5.1 Command out of sequence")
235
+ else
236
+ send_text(send(pattern[2], m[1].to_s.strip))
237
+ end
238
+ unrecognized = false
239
+ break
240
+ end
241
+ end
242
+ if unrecognized
243
+ response = "500 5.5.1 Unrecognized command #{text.inspect}, incorrectly formatted command, or command out of sequence"
244
+ send_text(response)
245
+ end
246
+ rescue OpenSSL::SSL::SSLError => e
247
+ LOG.error("%06d"%Process::pid) {"SSL error: #{e.inspect}"}
248
+ e.backtrace.each { |line| LOG.error("%06d"%Process::pid) {line} }
249
+ @done = true
250
+ end until @done
251
+
252
+ rescue Quit => e
253
+ @mail[:accepted] = false
254
+ # nothing to do but exit
255
+
256
+ rescue => e
257
+ # this is the "rescue of last resort"... "for when sh*t happens"
258
+ LOG.fatal("%06d"%Process::pid) {e.inspect}
259
+ e.backtrace.each { |line| LOG.fatal("%06d"%Process::pid) {line} }
260
+ @mail[:accepted] = false
261
+
262
+ ensure
263
+ # the email is either "received" or not, then when the
264
+ # return is executed, the process terminates
265
+ status = if @mail[:accepted] then 'Received' else 'Rejected' end
266
+ LOG.info("%06d"%Process::pid) {"#{status} mail with id '#{@mail[:id]}'"}
267
+ received(@mail)
268
+ # This is the end, beautiful friend
269
+ # This is the end, my only friend
270
+ # The end -- Jim Morrison
271
+ return nil # terminates the process
272
+ end
273
+
274
+ #=======================================================================
275
+ # these methods provide all the basic processing
276
+
277
+ def ok?(msg)
278
+ msg[0]!='4' && msg[0]!='5'
279
+ end
280
+
281
+ def do_connect(value)
282
+ LOG.info("%06d"%Process::pid) {"New item of mail opened with id '#{@mail[:id]}'"}
283
+ @mail[:connect] = p = {}
284
+ p[:value] = value
285
+
286
+ # this doesn't work with IPv4 addresses 'mapped' into IPv6, ie, ::ffff...
287
+ p[:domain] = value.dig_ptr
288
+
289
+ @level = 1 if ok?(msg = connect(p))
290
+ return msg
291
+ end
292
+
293
+ def do_ehlo(value)
294
+ @mail[:ehlo] = p = {}
295
+ p[:value] = value
296
+ p[:fip] = p[:rip] = nil
297
+ p[:rip] = rip = value.dig_a # reverse IP
298
+ p[:domain] = domain = rip.dig_ptr if rip
299
+ p[:fip] = domain.dig_a if domain # forward IP
300
+
301
+ return ("550 5.5.0 The domain name in EHLO does not validate") \
302
+ if @options[:ehlo_validation_check] && (p[:rip].nil? || p[:fip].nil? || p[:rip]!=p[:fip])
303
+
304
+ @level = 2 if ok?(msg = ehlo(p))
305
+ return msg
306
+ end
307
+
308
+ def do_quit(value)
309
+ @done = true if ok?(msg = quit(value))
310
+ return msg
311
+ end
312
+
313
+ def do_slam(value)
314
+ LOG.info("%06d"%Process::pid) {"Sender slammed the connection shut IP=#{@mail[:remote_ip]}"}
315
+ @done = true
316
+ @mail[:accepted] = false
317
+ return nil
318
+ end
319
+
320
+ def do_timeout(value)
321
+ @done = true
322
+ @mail[:accepted] = false
323
+ return ("501 5.4.7 Closing connection due to inactivity--#{@mail[:id]} was NOT saved")
324
+ end
325
+
326
+ def do_auth(value)
327
+ auth_type, auth_encoded = value.split
328
+ # auth_encoded contains both username and password
329
+ case auth_type.upcase
330
+ when "PLAIN"
331
+ # get the password hash from the database
332
+ username, ok = auth_encoded.validate_plain do |username|
333
+ password(username)
334
+ end
335
+ if ok
336
+ @mail[:authenticated] = username
337
+ return "235 2.0.0 Authentication succeeded"
338
+ else
339
+ return "530 5.7.5 Authentication failed"
340
+ end
341
+ else
342
+ return "504 5.7.6 authentication mechanism not supported"
343
+ end
344
+ end
345
+
346
+ def do_expn(value)
347
+ @mail[:expn] = p = {}
348
+ p[:value] = value
349
+ return expn(p)
350
+ end
351
+
352
+ def do_help(value)
353
+ return help(value)
354
+ end
355
+
356
+ def do_noop(value)
357
+ return noop(value)
358
+ end
359
+
360
+ def do_rset(value)
361
+ @level = 0 if ok?(msg = rset(value))
362
+ return msg
363
+ end
364
+
365
+ def do_vfry(value)
366
+ @mail[:vfry] = p = {}
367
+ p[:value] = value
368
+ return vfry(p)
369
+ end
370
+
371
+ def do_starttls(value)
372
+ send_text("220 2.0.0 TLS go ahead")
373
+ @connection.accept
374
+ @mail[:encrypted] = true
375
+ @enc_ind = '~'
376
+ return nil
377
+ end
378
+
379
+ def do_mail_from(value)
380
+ @mail[:mailfrom] = p = {:accepted=>false}
381
+ @mail[:rcptto] = []
382
+ # TODO! A special case is the NULL envelope sender address (i.e. MAIL FROM: <>)
383
+ msg = psych_value(:mailfrom, p, value)
384
+ return (msg) if msg
385
+
386
+ if ok?(msg = mail_from(p))
387
+ p[:accepted] = true
388
+ @level = 3
389
+ end
390
+ return msg
391
+ end
392
+
393
+ def do_rcpt_to(value)
394
+ @mail[:rcptto] ||= []
395
+ @mail[:rcptto] << p = {:accepted=>false}
396
+
397
+ msg = psych_value(:rcptto, p, value)
398
+ return (msg) if msg
399
+
400
+ if ok?(msg = rcpt_to(p))
401
+ p[:accepted] = true
402
+ @level = 4
403
+ end
404
+ return msg
405
+ end
406
+
407
+ def do_data(value)
408
+ # http://www.tldp.org/HOWTO/Spam-Filtering-for-MX/datachecks.html
409
+ @mail[:data] = body = {}
410
+ body[:accepted] = false
411
+ # receive the body of the mail
412
+ body[:value] = value # this should be nil -- no argument on the DATA command
413
+ body[:text] = lines = []
414
+ send_text("354 3.0.0 Enter message, ending with \".\" on a line by itself", false)
415
+ LOG.info("%06d"%Process::pid) {" -> (email message)"} if LogConversation
416
+ while true
417
+ text = recv_text(false)
418
+ break if text.nil? # the client closed the channel abruptly
419
+ lines << text
420
+ if text=="."
421
+ body[:accepted] = true
422
+ break
423
+ end
424
+ end
425
+ @mail.parse_headers
426
+ # should contain:
427
+ # To: ...
428
+ # Date: ...
429
+ # From: ...
430
+ # Subject: ...
431
+ # Message-ID: ...
432
+
433
+ # DKIM
434
+ # SPF
435
+
436
+ # check the DKIM headers, if any
437
+ ok, signatures = pdkim_verify_an_email(PDKIM_INPUT_NORMAL, @mail[:data][:text])
438
+ signatures.each do |signature|
439
+ @log.info("%06d"%Process::pid){"Signature for '#{signature[:domain]}': #{PdkimReturnCodes[signature[:verify_status]]}"}
440
+ @mail[:signatures] ||= []
441
+ @mail[:signatures] << [signature[:domain], signature[:verify_status], DkimOutcomes[signature[:verify_status]]]
442
+ end if ok==PDKIM_OK
443
+
444
+ # test all the RCPT TOs
445
+ all_rcptto_accepted = true
446
+ @mail[:rcptto].each { |p| all_rcptto_accepted = false if !p[:accepted] } if @mail.has_key?(:rcptto)
447
+ # passed thru the guantlet with no failures
448
+ @mail[:accepted] = true \
449
+ if @mail[:mailfrom][:accepted] &&
450
+ all_rcptto_accepted &&
451
+ @mail[:data][:accepted] &&
452
+ @has_level_5_warnings==false
453
+
454
+ msg = data(p)
455
+ @level = 1
456
+ return msg
457
+ end
458
+
459
+ #=======================================================================
460
+ # these are the defaults, in case the user doesn't override
461
+
462
+ def connect(remote_ip)
463
+ return "220 2.0.0 ESMTP RubyMTA 0.01 #{Time.new.strftime("%^a, %d %^b %Y %H:%M:%S %z")}"
464
+ end
465
+
466
+ def ehlo(p)
467
+ msg = ["250-2.0.0 #{p[:value]} Hello"]
468
+ msg << "250-STARTTLS" if !@mail[:encrypted]
469
+ msg << "250-AUTH PLAIN"
470
+ msg << "250 HELP"
471
+ return msg
472
+ end
473
+
474
+ def quit(value)
475
+ return "221 2.0.0 OK #{"example.com"} closing connection"
476
+ end
477
+
478
+ def auth(value)
479
+ return "235 2.0.0 Authentication succeeded"
480
+ end
481
+
482
+ def password(username)
483
+ return nil
484
+ end
485
+
486
+ def expn(value)
487
+ return "252 2.5.1 Administrative prohibition"
488
+ end
489
+
490
+ def help(value)
491
+ return "250 2.0.0 QUIT AUTH, EHLO, EXPN, HELO, HELP, NOOP, RSET, VFRY, STARTTLS, MAIL FROM, RCPT TO, DATA"
492
+ end
493
+
494
+ def noop(value)
495
+ return "250 2.0.0 OK"
496
+ end
497
+
498
+ def rset(value)
499
+ return "250 2.0.0 Reset OK"
500
+ end
501
+
502
+ def vfry(value)
503
+ return "252 2.5.1 Administrative prohibition"
504
+ end
505
+
506
+ def mail_from(value)
507
+ return "250 2.0.0 OK"
508
+ end
509
+
510
+ def rcpt_to(value)
511
+ return "250 2.0.0 OK"
512
+ end
513
+
514
+ def data(value)
515
+ return "250 2.0.0 OK id=#{@mail[:id]}"
516
+ end
517
+
518
+ def received(mail)
519
+ # nothing here--just a placeholder
520
+ end
521
+ end
522
+ end
@@ -0,0 +1,4 @@
1
+ module Net
2
+ BUILD_VERSION = "1.0.0"
3
+ BUILD_DATE = "2016-09-21"
4
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: net-receiver
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael J. Welch, Ph.D.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-09-21 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Ruby Net Receiver.
14
+ email:
15
+ - mjwelchphd@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - CHANGELOG.md
21
+ - README.md
22
+ - lib/net/extended_classes.rb
23
+ - lib/net/item_of_mail.rb
24
+ - lib/net/receiver.rb
25
+ - lib/net/version.rb
26
+ homepage: https://github.com/mjwelchphd/net-receiver
27
+ licenses:
28
+ - MIT
29
+ metadata: {}
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 2.4.6
47
+ signing_key:
48
+ specification_version: 4
49
+ summary: Ruby Net Receiver.
50
+ test_files: []