Haraka 3.1.5 → 3.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +1 -1
- package/{Changes.md → CHANGELOG.md} +22 -2
- package/CONTRIBUTORS.md +26 -26
- package/README.md +68 -93
- package/SECURITY.md +178 -0
- package/bin/haraka +7 -14
- package/config/plugins +0 -3
- package/docs/Connection.md +126 -39
- package/docs/CoreConfig.md +92 -74
- package/docs/HAProxy.md +41 -25
- package/docs/Logging.md +68 -38
- package/docs/Outbound.md +124 -179
- package/docs/Plugins.md +38 -59
- package/docs/Transaction.md +78 -83
- package/docs/Tutorial.md +122 -209
- package/docs/plugins/aliases.md +1 -141
- package/docs/plugins/auth/auth_ldap.md +2 -39
- package/docs/plugins/max_unrecognized_commands.md +4 -18
- package/docs/plugins/process_title.md +3 -3
- package/docs/plugins/reseed_rng.md +11 -13
- package/docs/plugins/tls.md +7 -7
- package/docs/plugins/toobusy.md +10 -4
- package/docs/tutorials/SettingUpOutbound.md +40 -48
- package/endpoint.js +32 -2
- package/outbound/hmail.js +3 -2
- package/outbound/index.js +3 -0
- package/package.json +19 -30
- package/server.js +17 -7
- package/test/connection.js +234 -0
- package/test/endpoint.js +27 -0
- package/test/outbound/hmail.js +19 -0
- package/test/outbound/index.js +189 -0
- package/test/outbound/queue.js +92 -0
- package/test/server.js +172 -0
- package/test/tls_socket.js +138 -0
- package/tls_socket.js +2 -2
package/docs/Transaction.md
CHANGED
|
@@ -1,140 +1,135 @@
|
|
|
1
1
|
# Transaction Object
|
|
2
2
|
|
|
3
|
-
An SMTP transaction
|
|
3
|
+
An SMTP transaction begins at `MAIL FROM` and ends at `RSET` or end-of-data (the "final dot"). A connection can carry multiple transactions; the current one is available as `connection.transaction`.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Properties
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### transaction.uuid
|
|
8
8
|
|
|
9
|
-
A unique UUID for this transaction
|
|
9
|
+
A unique UUID for this transaction, of the form `<connection.uuid>.N`, where `N` increments per transaction on the connection.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
### transaction.mail_from
|
|
12
12
|
|
|
13
|
-
The
|
|
13
|
+
The `MAIL FROM` argument as an [`Address`][address] object.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
### transaction.rcpt_to
|
|
16
16
|
|
|
17
|
-
An
|
|
17
|
+
An array of [`Address`][address] objects, one per accepted `RCPT TO`.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
### transaction.header
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
The parsed message header. See [haraka-email-message → Header](https://github.com/haraka/email-message#header).
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
### transaction.body
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
transaction.message_stream.pipe(WritableStream, options)
|
|
27
|
-
```
|
|
25
|
+
The parsed message body, available only when `parse_body` is `true`. See [haraka-email-message → Body](https://github.com/haraka/email-message#body).
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
### transaction.message_stream
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
A Node.js `Readable` stream for the message (headers + body). Pipe it into any `Writable` — a socket, file, stdout, or your own stream:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
transaction.message_stream.pipe(writable, options)
|
|
33
|
+
```
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
* dot_stuffed (default: true)
|
|
35
|
-
* ending_dot (default: false)
|
|
36
|
-
* end (default: true)
|
|
37
|
-
* buffer_size (default: 65535)
|
|
38
|
-
* clamd_style (default: false)
|
|
35
|
+
`options` may override:
|
|
39
36
|
|
|
40
|
-
|
|
37
|
+
| Option | Default | Description |
|
|
38
|
+
| -------------- | -------- | --- |
|
|
39
|
+
| `line_endings` | `"\r\n"` | newline sequence |
|
|
40
|
+
| `dot_stuffed` | `true` | emit SMTP dot-stuffed output |
|
|
41
|
+
| `ending_dot` | `false` | terminate with `.\r\n` (SMTP end-of-data) |
|
|
42
|
+
| `end` | `true` | call `.end()` on the writable when finished |
|
|
43
|
+
| `buffer_size` | `65535` | internal buffer size |
|
|
44
|
+
| `clamd_style` | `false` | ClamAV CLAMSCAN-INSTREAM framing |
|
|
41
45
|
|
|
42
46
|
```js
|
|
43
47
|
transaction.message_stream.pipe(socket, { ending_dot: true })
|
|
44
48
|
```
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
### transaction.data_bytes
|
|
51
|
+
|
|
52
|
+
Number of bytes received during `DATA`.
|
|
53
|
+
|
|
54
|
+
### transaction.parse_body
|
|
55
|
+
|
|
56
|
+
`false` by default. Set to `true` (in `hook_data` or earlier) to enable MIME body parsing, after which `transaction.body` becomes available. `attachment_hooks()`, `set_banner()`, and `add_body_filter()` set this automatically.
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
### transaction.discard_data
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
Set to `true` to drop the raw message as it arrives instead of buffering it in `message_stream`. The parsed body and attachments are still available when `parse_body` is `true`. Useful for plugins that only need attachments or text without retaining the whole message.
|
|
51
61
|
|
|
52
|
-
|
|
62
|
+
### transaction.notes
|
|
53
63
|
|
|
54
|
-
- transaction.notes
|
|
64
|
+
A `haraka-notes` instance scoped to this transaction. Use it to pass state between hooks; for structured per-test output prefer `transaction.results`. See [haraka-notes](https://github.com/haraka/haraka-notes).
|
|
55
65
|
|
|
56
|
-
|
|
66
|
+
`transaction.notes.skip_plugins` is honoured by the plugin runner — push plugin names into it to bypass them for the remainder of the transaction.
|
|
57
67
|
|
|
58
|
-
|
|
68
|
+
### transaction.results
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
Structured store for plugin results. See [haraka-results](https://github.com/haraka/haraka-results).
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
### transaction.rcpt_count
|
|
63
73
|
|
|
64
|
-
|
|
74
|
+
Per-disposition counters (`accept`, `tempfail`, `reject`) tracking recipients in this transaction.
|
|
65
75
|
|
|
66
|
-
|
|
76
|
+
### transaction.mime_part_count
|
|
67
77
|
|
|
68
|
-
|
|
78
|
+
Number of MIME parts seen so far (when `parse_body` is enabled).
|
|
69
79
|
|
|
70
|
-
|
|
80
|
+
### transaction.encoding
|
|
71
81
|
|
|
72
|
-
|
|
82
|
+
Character encoding used to convert incoming bytes to strings. Defaults to `'utf8'`.
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
## Methods
|
|
75
85
|
|
|
76
|
-
|
|
86
|
+
### transaction.add_header(key, value)
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
Append a header to the message.
|
|
79
89
|
|
|
80
|
-
|
|
90
|
+
### transaction.add_leading_header(key, value)
|
|
81
91
|
|
|
82
|
-
|
|
92
|
+
Prepend a header to the message. Most plugins want `add_header()`; use this only when ordering matters (e.g. `Received:` chains).
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
### transaction.remove_header(key)
|
|
85
95
|
|
|
86
|
-
|
|
96
|
+
Remove all headers with `key`.
|
|
87
97
|
|
|
88
|
-
|
|
98
|
+
### transaction.add_data(line)
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
Append a raw line to the message. The input must already be in SMTP wire format (CRLF newlines, dot-stuffed). Not the right tool for adding banners or transforming body parts — see `set_banner()` and `add_body_filter()`.
|
|
101
|
+
|
|
102
|
+
### transaction.attachment_hooks(start)
|
|
103
|
+
|
|
104
|
+
Register a callback fired for each attachment. `start` is called with `(content_type, filename, body, stream)`; `stream` is a Node.js `Readable`. Setting `stream.connection = connection` applies backpressure to the SMTP connection so attachments can be processed before the message ends.
|
|
91
105
|
|
|
92
106
|
```js
|
|
93
|
-
exports.hook_data =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
next()
|
|
107
|
+
exports.hook_data = (next, connection) => {
|
|
108
|
+
connection.transaction.attachment_hooks((ct, fn, body, stream) => {
|
|
109
|
+
start_att(connection, ct, fn, body, stream)
|
|
110
|
+
})
|
|
111
|
+
next()
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
function start_att(connection, ct, fn, body, stream) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const ws = fs.createWriteStream(path)
|
|
111
|
-
stream.pipe(ws)
|
|
112
|
-
stream.resume()
|
|
113
|
-
ws.on('close', () => {
|
|
114
|
-
connection.loginfo('End of stream reached')
|
|
115
|
-
fs.fstat(fd, (err, stats) => {
|
|
116
|
-
connection.loginfo(`Got data of length: ${stats.size}`)
|
|
117
|
-
fs.close(fd, () => {}) // Close the tmp file descriptor
|
|
118
|
-
})
|
|
115
|
+
connection.loginfo(`attachment: ${ct} ${fn}`)
|
|
116
|
+
stream.connection = connection // enable backpressure
|
|
117
|
+
stream.pause()
|
|
118
|
+
|
|
119
|
+
require('node:tmp').file((err, path, fd) => {
|
|
120
|
+
const ws = require('node:fs').createWriteStream(path)
|
|
121
|
+
stream.pipe(ws)
|
|
122
|
+
stream.resume()
|
|
119
123
|
})
|
|
120
|
-
})
|
|
121
124
|
}
|
|
122
125
|
```
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
Set this flag to true to discard all data as it arrives and not store in memory or on disk (in the message_stream property). You can still access the attachments and body if you set parse_body to true. This is useful for systems which do not need the full email, just the attachments or mail text.
|
|
127
|
-
|
|
128
|
-
- transaction.set_banner(text, html)
|
|
129
|
-
|
|
130
|
-
Sets a banner to be added to the end of the email. If the html part is not given (optional) then the text part will have each line ending replaced with `<br/>` when being inserted into HTML parts.
|
|
131
|
-
|
|
132
|
-
- transaction.add_body_filter(ct_match, filter)
|
|
127
|
+
### transaction.set_banner(text, html)
|
|
133
128
|
|
|
134
|
-
|
|
129
|
+
Append a banner to the end of the message. If `html` is omitted, each newline in `text` is replaced with `<br/>\n` when inserted into HTML parts.
|
|
135
130
|
|
|
136
|
-
|
|
131
|
+
### transaction.add_body_filter(ct_match, filter)
|
|
137
132
|
|
|
138
|
-
|
|
133
|
+
Register a filter applied to body parts. `ct_match` is either a regex matched against the content-type line, or a string matched as a prefix (e.g. `/^text\/html/` or `'text/plain'`). `filter` receives `(content_type, encoding, buffer)` and must return a `Buffer` with the replacement body (in the same encoding).
|
|
139
134
|
|
|
140
|
-
[
|
|
135
|
+
[address]: https://github.com/haraka/node-address-rfc2821
|
package/docs/Tutorial.md
CHANGED
|
@@ -1,270 +1,183 @@
|
|
|
1
1
|
# Writing Haraka Plugins
|
|
2
2
|
|
|
3
|
-
Part of the joy of using Haraka as your main mail server is having a strong
|
|
4
|
-
plugin based system which means you control all aspects of how your mail is
|
|
5
|
-
processed, accepted, and delivered.
|
|
3
|
+
Part of the joy of using Haraka as your main mail server is having a strong plugin system: you control every aspect of how mail is processed, accepted, and delivered.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
sort of plugin file of your own to customise how things work. The good news
|
|
9
|
-
is that writing plugins in Haraka is simple, even for novice coders. You
|
|
10
|
-
just need a little knowledge of Javascript (and maybe some understanding of
|
|
11
|
-
Node.js) and the world is your oyster.
|
|
5
|
+
This tutorial walks through a small plugin that supports *disposable addresses*: an email like `user-20271231@example.com` is accepted up until 31 December 2027, after which delivery is rejected. Mail that is still within the validity window is rewritten back to `user@example.com` before it is forwarded on.
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
email addresses that expire in a short period of time. This is handy if you
|
|
15
|
-
want a _disposable email address_ to use to sign up for a web site that you
|
|
16
|
-
don't wish to continually receive communication from.
|
|
7
|
+
## What You'll Need
|
|
17
8
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
In order to make this simple, we are going to simply let you have tagged
|
|
21
|
-
email addresses such as `user-20120515@domain.com` which will expire on the
|
|
22
|
-
15th May, 2012. Haraka will then check the email has yet to expire, and
|
|
23
|
-
reject mails to that address after the expiry date. If the address hasn't
|
|
24
|
-
expired yet it will re-write the address to `user@domain.com` before onward
|
|
25
|
-
delivery.
|
|
26
|
-
|
|
27
|
-
## What You Will Need
|
|
28
|
-
|
|
29
|
-
- Node.js and npm
|
|
9
|
+
- Node.js (an active LTS release) and npm
|
|
30
10
|
- Haraka
|
|
31
11
|
- A text editor
|
|
32
|
-
- [swaks][
|
|
33
|
-
- A screwdriver
|
|
34
|
-
|
|
35
|
-
[1]: http://jetmore.org/john/code/swaks/
|
|
12
|
+
- [swaks][swaks] for sending test mail
|
|
36
13
|
|
|
37
14
|
## Getting Started
|
|
38
15
|
|
|
39
|
-
|
|
16
|
+
Install Haraka and create a project:
|
|
40
17
|
|
|
41
|
-
|
|
18
|
+
```sh
|
|
19
|
+
sudo npm install -g Haraka
|
|
20
|
+
haraka -i /path/to/new_project
|
|
21
|
+
```
|
|
42
22
|
|
|
43
|
-
|
|
23
|
+
Use a directory that does not yet exist. Now scaffold a plugin:
|
|
44
24
|
|
|
45
|
-
|
|
25
|
+
```sh
|
|
26
|
+
haraka -c /path/to/new_project -p rcpt_to.disposable
|
|
27
|
+
```
|
|
46
28
|
|
|
47
|
-
|
|
29
|
+
`haraka -p` reports the files it created:
|
|
48
30
|
|
|
49
|
-
|
|
31
|
+
```
|
|
32
|
+
Plugin rcpt_to.disposable created
|
|
33
|
+
Now edit javascript in: /path/to/new_project/plugins/rcpt_to.disposable.js
|
|
34
|
+
Add the plugin to config: /path/to/new_project/config/plugins
|
|
35
|
+
And edit documentation in: /path/to/new_project/docs/plugins/rcpt_to.disposable.md
|
|
36
|
+
```
|
|
50
37
|
|
|
51
|
-
|
|
38
|
+
Edit `config/plugins` so the only enabled lines are:
|
|
52
39
|
|
|
53
|
-
|
|
40
|
+
```
|
|
41
|
+
rcpt_to.disposable
|
|
42
|
+
rcpt_to.in_host_list
|
|
43
|
+
queue/test
|
|
44
|
+
```
|
|
54
45
|
|
|
55
|
-
|
|
56
|
-
Now edit javascript in: /path/to/new_project/plugins/rcpt_to.disposable.js
|
|
57
|
-
Add the plugin to config: /path/to/new_project/config/plugins
|
|
58
|
-
And edit documentation in: /path/to/new_project/docs/plugins/rcpt_to.disposable.md
|
|
46
|
+
The ordering matters — the disposable plugin must run *before* `rcpt_to.in_host_list`, which accepts mail for domains listed in `config/host_list`. `queue/test` writes accepted mail to a `.eml` file in `os.tmpdir()` so you can confirm delivery.
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
set it up to test. Comment out most of the plugins, except for
|
|
62
|
-
`rcpt_to.in_host_list` and add in our new plugin, and change the queue
|
|
63
|
-
plugin to `test_queue`. The final file should look like this:
|
|
48
|
+
Open `plugins/rcpt_to.disposable.js` and start with:
|
|
64
49
|
|
|
65
|
-
|
|
50
|
+
```js
|
|
51
|
+
exports.hook_rcpt = (next, connection, params) => {
|
|
52
|
+
const rcpt = params[0]
|
|
53
|
+
connection.loginfo(`got recipient: ${rcpt}`)
|
|
54
|
+
next()
|
|
55
|
+
}
|
|
56
|
+
```
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
#data.signatures
|
|
58
|
+
Verify it works. In one terminal:
|
|
69
59
|
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
```sh
|
|
61
|
+
echo LOGDEBUG > config/loglevel
|
|
62
|
+
echo myserver.com >> config/host_list
|
|
63
|
+
sudo haraka -c /path/to/new_project
|
|
64
|
+
```
|
|
72
65
|
|
|
73
|
-
|
|
74
|
-
#mail_from.is_resolvable
|
|
66
|
+
In another:
|
|
75
67
|
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
```sh
|
|
69
|
+
swaks -h example.com -t booya@myserver.com -f sender@example.com -s localhost -p 25
|
|
70
|
+
```
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
rcpt_to.in_host_list
|
|
72
|
+
You should see something like this in the Haraka log:
|
|
81
73
|
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
```
|
|
75
|
+
[INFO] [<uuid>] [rcpt_to.disposable] got recipient: <booya@myserver.com>
|
|
76
|
+
```
|
|
84
77
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
The ordering here is important - our new plugin has to come before `rcpt_to.in_host_list`.
|
|
88
|
-
|
|
89
|
-
Fire up your favourite editor and put the following into the `plugins/rcpt_to.disposable.js` file:
|
|
90
|
-
|
|
91
|
-
exports.hook_rcpt = function (next, connection, params) {
|
|
92
|
-
const rcpt = params[0];
|
|
93
|
-
this.loginfo("Got recipient: " + rcpt);
|
|
94
|
-
next();
|
|
95
|
-
}
|
|
78
|
+
…and a `.eml` file in your system temp directory containing the message.
|
|
96
79
|
|
|
97
|
-
|
|
80
|
+
## Parsing Out the Date
|
|
98
81
|
|
|
99
|
-
|
|
82
|
+
Detect addresses of the form `user-YYYYMMDD` and parse the date:
|
|
100
83
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
84
|
+
```js
|
|
85
|
+
exports.hook_rcpt = (next, connection, params) => {
|
|
86
|
+
const rcpt = params[0]
|
|
87
|
+
connection.loginfo(`got recipient: ${rcpt}`)
|
|
104
88
|
|
|
105
|
-
|
|
89
|
+
const match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user)
|
|
90
|
+
if (!match) return next()
|
|
106
91
|
|
|
107
|
-
|
|
108
|
-
|
|
92
|
+
// Date constructor uses zero-indexed months (Dec === 11)
|
|
93
|
+
const expiry = new Date(match[2], match[3] - 1, match[4])
|
|
94
|
+
connection.loginfo(`expires on: ${expiry.toISOString()}`)
|
|
109
95
|
|
|
110
|
-
|
|
96
|
+
next()
|
|
97
|
+
}
|
|
98
|
+
```
|
|
111
99
|
|
|
112
|
-
|
|
100
|
+
Restart Haraka and send:
|
|
113
101
|
|
|
114
|
-
|
|
115
|
-
|
|
102
|
+
```sh
|
|
103
|
+
swaks -h example.com -t booya-20271231@myserver.com \
|
|
104
|
+
-f sender@example.com -s localhost -p 25
|
|
105
|
+
```
|
|
116
106
|
|
|
117
|
-
|
|
107
|
+
Logs:
|
|
118
108
|
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
```
|
|
110
|
+
[INFO] [rcpt_to.disposable] got recipient: <booya-20271231@myserver.com>
|
|
111
|
+
[INFO] [rcpt_to.disposable] expires on: 2027-12-31T00:00:00.000Z
|
|
112
|
+
```
|
|
121
113
|
|
|
122
|
-
|
|
123
|
-
var rcpt = params[0];
|
|
124
|
-
this.loginfo("Got recipient: " + rcpt);
|
|
114
|
+
## Rejecting Expired Addresses
|
|
125
115
|
|
|
126
|
-
|
|
127
|
-
var match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user);
|
|
128
|
-
if (!match) {
|
|
129
|
-
return next();
|
|
130
|
-
}
|
|
116
|
+
Compare the parsed date to today and reject if it has already passed:
|
|
131
117
|
|
|
132
|
-
|
|
133
|
-
|
|
118
|
+
```js
|
|
119
|
+
exports.hook_rcpt = (next, connection, params) => {
|
|
120
|
+
const rcpt = params[0]
|
|
134
121
|
|
|
135
|
-
|
|
122
|
+
const match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user)
|
|
123
|
+
if (!match) return next()
|
|
136
124
|
|
|
137
|
-
|
|
125
|
+
const expiry = new Date(match[2], match[3] - 1, match[4])
|
|
126
|
+
if (expiry < new Date()) {
|
|
127
|
+
return next(DENY, 'Expired email address')
|
|
138
128
|
}
|
|
139
129
|
|
|
140
|
-
|
|
130
|
+
next()
|
|
131
|
+
}
|
|
132
|
+
```
|
|
141
133
|
|
|
142
|
-
|
|
143
|
-
-f somewhere@example.com -s localhost -p 25
|
|
134
|
+
Send mail to an expired address:
|
|
144
135
|
|
|
145
|
-
|
|
136
|
+
```sh
|
|
137
|
+
swaks -h example.com -t booya-20200101@myserver.com \
|
|
138
|
+
-f sender@example.com -s localhost -p 25
|
|
139
|
+
```
|
|
146
140
|
|
|
147
|
-
|
|
148
|
-
[INFO] [rcpt_to.disposable] Email expires on: Sun, 01 Jan 2012 05:00:00 GMT
|
|
141
|
+
The remote end sees:
|
|
149
142
|
|
|
150
|
-
|
|
151
|
-
|
|
143
|
+
```
|
|
144
|
+
<** 550 Expired email address
|
|
145
|
+
```
|
|
152
146
|
|
|
153
|
-
##
|
|
147
|
+
## Rewriting Live Addresses
|
|
154
148
|
|
|
155
|
-
|
|
156
|
-
and reject expired emails. Again, this is very simple:
|
|
149
|
+
When the address is still valid, strip the date tag so the downstream mail store receives plain `user@domain`:
|
|
157
150
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
```js
|
|
152
|
+
exports.hook_rcpt = (next, connection, params) => {
|
|
153
|
+
const rcpt = params[0]
|
|
161
154
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (!match) {
|
|
165
|
-
return next();
|
|
166
|
-
}
|
|
155
|
+
const match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user)
|
|
156
|
+
if (!match) return next()
|
|
167
157
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this.loginfo("Email expires on: " + expiry_date);
|
|
172
|
-
|
|
173
|
-
var today = new Date();
|
|
174
|
-
|
|
175
|
-
if (expiry_date < today) {
|
|
176
|
-
// If we get here, the email address has expired
|
|
177
|
-
return next(DENY, "Expired email address");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
next();
|
|
158
|
+
const expiry = new Date(match[2], match[3] - 1, match[4])
|
|
159
|
+
if (expiry < new Date()) {
|
|
160
|
+
return next(DENY, 'Expired email address')
|
|
181
161
|
}
|
|
182
162
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
=== Connected to localhost.
|
|
189
|
-
<- 220 sergeant.org ESMTP Haraka 0.3 ready
|
|
190
|
-
-> EHLO foo.com
|
|
191
|
-
<- 250-Haraka says hi Unknown [127.0.0.1]
|
|
192
|
-
<- 250-PIPELINING
|
|
193
|
-
<- 250-8BITMIME
|
|
194
|
-
<- 250 SIZE 500000
|
|
195
|
-
-> MAIL FROM:<somewhere@example.com>
|
|
196
|
-
<- 250 From address is OK
|
|
197
|
-
-> RCPT TO:<booya-20110101@haraka.local>
|
|
198
|
-
<** 550 Expired email address
|
|
199
|
-
-> QUIT
|
|
200
|
-
<- 221 closing connection. Have a jolly good day.
|
|
201
|
-
=== Connection closed with remote host.
|
|
202
|
-
|
|
203
|
-
Now we need to do one more thing...
|
|
204
|
-
|
|
205
|
-
## Fixing Up Unexpired Emails
|
|
206
|
-
|
|
207
|
-
The last thing we need to do, is if we have an email that isn't expired, we
|
|
208
|
-
need to normalise it back to the real email address, because wherever we
|
|
209
|
-
deliver this to is unlikely to recognise these new email addresses.
|
|
210
|
-
|
|
211
|
-
Here's how our final plugin will look:
|
|
212
|
-
|
|
213
|
-
exports.hook_rcpt = function (next, connection, params) {
|
|
214
|
-
var rcpt = params[0];
|
|
215
|
-
this.loginfo("Got recipient: " + rcpt);
|
|
216
|
-
|
|
217
|
-
// Check user matches regex 'user-YYYYMMDD':
|
|
218
|
-
var match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user);
|
|
219
|
-
if (!match) {
|
|
220
|
-
return next();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// get date - note Date constructor takes month-1 (i.e. Dec == 11).
|
|
224
|
-
var expiry_date = new Date(match[2], match[3]-1, match[4]);
|
|
225
|
-
|
|
226
|
-
this.loginfo("Email expires on: " + expiry_date);
|
|
227
|
-
|
|
228
|
-
var today = new Date();
|
|
229
|
-
|
|
230
|
-
if (expiry_date < today) {
|
|
231
|
-
// If we get here, the email address has expired
|
|
232
|
-
return next(DENY, "Expired email address");
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// now get rid of the extension:
|
|
236
|
-
rcpt.user = match[1];
|
|
237
|
-
this.loginfo("Email address now: " + rcpt);
|
|
238
|
-
|
|
239
|
-
next();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
And when we test this with an unexpired address via swaks:
|
|
243
|
-
|
|
244
|
-
$ swaks -h foo.com -t booya-20120101@haraka.local \
|
|
245
|
-
-f somewhere@example.com -s localhost -p 25
|
|
246
|
-
|
|
247
|
-
We get in the logs:
|
|
163
|
+
rcpt.user = match[1]
|
|
164
|
+
connection.loginfo(`rewrote recipient to: ${rcpt}`)
|
|
165
|
+
next()
|
|
166
|
+
}
|
|
167
|
+
```
|
|
248
168
|
|
|
249
|
-
|
|
250
|
-
[INFO] [rcpt_to.disposable] Email expires on: Sun Jan 01 2012 00:00:00 GMT-0500 (EST)
|
|
251
|
-
[INFO] [rcpt_to.disposable] Email address now: <booya@haraka.local>
|
|
169
|
+
Send to a live tagged address and watch the log:
|
|
252
170
|
|
|
253
|
-
|
|
171
|
+
```
|
|
172
|
+
[INFO] [rcpt_to.disposable] rewrote recipient to: <booya@myserver.com>
|
|
173
|
+
```
|
|
254
174
|
|
|
255
|
-
|
|
175
|
+
## Further Reading
|
|
256
176
|
|
|
257
|
-
|
|
258
|
-
to the body of the email and the headers, access to the HELO string, and
|
|
259
|
-
implementing ESMTP extensions, among many others.
|
|
177
|
+
The Haraka API offers much more — body and header access, ESMTP extension hooks, the outbound delivery hooks, structured results, attachments, and so on. Two good starting points:
|
|
260
178
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
179
|
+
- The [Plugins guide](Plugins.md) and the rest of the `docs/` directory.
|
|
180
|
+
- The [Plugin registry](https://github.com/haraka/Haraka/blob/master/PLUGINS.md) for an inventory of real-world plugins.
|
|
181
|
+
- The plugins shipped in the [`plugins/`](../plugins/) directory. Even the most elaborate are under 200 lines; many are under 20.
|
|
264
182
|
|
|
265
|
-
|
|
266
|
-
The plugins that Haraka ships with use almost all parts of the API and so
|
|
267
|
-
should give you a good starting point if you want to implement a particular
|
|
268
|
-
piece of functionality. Even the most complicated plugins are under 200 lines
|
|
269
|
-
of code, so don't be intimidated by them! The simplest one is a mere 5 lines
|
|
270
|
-
of code.
|
|
183
|
+
[swaks]: https://www.jetmore.org/john/code/swaks/
|