net-receiver 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2 -0
- data/README.md +340 -0
- data/lib/net/extended_classes.rb +182 -0
- data/lib/net/item_of_mail.rb +77 -0
- data/lib/net/receiver.rb +522 -0
- data/lib/net/version.rb +4 -0
- metadata +50 -0
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
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
|
data/lib/net/receiver.rb
ADDED
@@ -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
|
data/lib/net/version.rb
ADDED
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: []
|