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.
@@ -1,140 +1,135 @@
1
1
  # Transaction Object
2
2
 
3
- An SMTP transaction is valid from MAIL FROM time until RSET or "final-dot".
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
- ## API
5
+ ## Properties
6
6
 
7
- - transaction.uuid
7
+ ### transaction.uuid
8
8
 
9
- A unique UUID for this transaction. Is equal to the connection.uuid + '.N' where N increments for each transaction on this connection.
9
+ A unique UUID for this transaction, of the form `<connection.uuid>.N`, where `N` increments per transaction on the connection.
10
10
 
11
- - transaction.mail_from
11
+ ### transaction.mail_from
12
12
 
13
- The value of the MAIL FROM command is an `Address`[1] object.
13
+ The `MAIL FROM` argument as an [`Address`][address] object.
14
14
 
15
- - transaction.rcpt_to
15
+ ### transaction.rcpt_to
16
16
 
17
- An Array of `Address`[1] objects of recipients from the RCPT TO command.
17
+ An array of [`Address`][address] objects, one per accepted `RCPT TO`.
18
18
 
19
- - transaction.message_stream
19
+ ### transaction.header
20
20
 
21
- A node.js Readable Stream object for the message.
21
+ The parsed message header. See [haraka-email-message Header](https://github.com/haraka/email-message#header).
22
22
 
23
- You use it like this:
23
+ ### transaction.body
24
24
 
25
- ```js
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
- Where WritableStream is a node.js Writable Stream object such as a net.socket, fs.writableStream, process.stdout/stderr or custom stream.
27
+ ### transaction.message_stream
30
28
 
31
- The options argument should be an object that overrides the following properties:
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
- * line_endings (default: "\r\n")
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
- e.g.
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
- - transaction.data_bytes
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
- The number of bytes in the email after DATA.
58
+ ### transaction.discard_data
49
59
 
50
- - transaction.add_data(line)
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
- Adds a line of data to the email. Note this is RAW email - it isn't useful for adding banners to the email.
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
- A safe place to store transaction specific values. See also [haraka-results](https://github.com/haraka/haraka-results) and [haraka-notes](https://github.com/haraka/haraka-notes).
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
- - transaction.add_leading_header(key, value)
68
+ ### transaction.results
59
69
 
60
- Adds a header to the top of the header list. This should only be used in very specific cases. Most cases will use `add_header()` instead.
70
+ Structured store for plugin results. See [haraka-results](https://github.com/haraka/haraka-results).
61
71
 
62
- - transaction.add_header(key, value)
72
+ ### transaction.rcpt_count
63
73
 
64
- Adds a header to the email.
74
+ Per-disposition counters (`accept`, `tempfail`, `reject`) tracking recipients in this transaction.
65
75
 
66
- - transaction.remove_header(key)
76
+ ### transaction.mime_part_count
67
77
 
68
- Deletes a header from the email.
78
+ Number of MIME parts seen so far (when `parse_body` is enabled).
69
79
 
70
- - transaction.header
80
+ ### transaction.encoding
71
81
 
72
- The header of the email. See `Header Object`.
82
+ Character encoding used to convert incoming bytes to strings. Defaults to `'utf8'`.
73
83
 
74
- - transaction.parse_body = true|false [default: false]
84
+ ## Methods
75
85
 
76
- Set to `true` to enable parsing of the mail body. Make sure you set this in hook_data or before. Storing a transaction hook (with transaction.attachment_hooks) will set this to true.
86
+ ### transaction.add_header(key, value)
77
87
 
78
- - transaction.body
88
+ Append a header to the message.
79
89
 
80
- The body of the email if you set `parse_body` above. See `Body Object`.
90
+ ### transaction.add_leading_header(key, value)
81
91
 
82
- - transaction.attachment_hooks(start)
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
- Sets a callback for when we see an attachment.
94
+ ### transaction.remove_header(key)
85
95
 
86
- The `start` event will receive `(content_type, filename, body, stream)` as parameters.
96
+ Remove all headers with `key`.
87
97
 
88
- The stream is a [ReadableStream](http://nodejs.org/api/stream.html)
98
+ ### transaction.add_data(line)
89
99
 
90
- If you set stream.connection then the stream will apply backpressure to the connection, allowing you to process attachments before the connection has ended. Here is an example which stores attachments in temporary files using the `tmp` library from npm and tells us the size of the file:
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 = function (next, connection) {
94
- // enable mail body parsing
95
- connection.transaction.attachment_hooks(function (ct, fn, body, stream) {
96
- start_att(connection, ct, fn, body, stream)
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
- connection.loginfo(`Got attachment: ${ct}, ${fn} for user id: ${connection.transaction.notes.hubdoc_user.email}`)
103
- connection.transaction.notes.attachment_count++
104
-
105
- stream.connection = connection // Allow backpressure
106
- stream.pause()
107
-
108
- require('tmp').file((err, path, fd) => {
109
- connection.loginfo(`Got tempfile: ${path} (${fd})`)
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
- - transaction.discard_data = true|false [default: false]
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
- Adds a filter to be applied to body parts in the email. ct_match should be a regular expression to match against the full content-type line, or a string to match at the start, e.g. `/^text\/html/` or `'text/plain'`. filter will be called when each body part matching ct_match is complete. It receives three parameters: the content-type line, the encoding name, and a buffer with the full body part. It should return a buffer with the desired contents of the body in the same encoding.
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
- - transaction.results
131
+ ### transaction.add_body_filter(ct_match, filter)
137
132
 
138
- Store [results](https://github.com/haraka/haraka-results) of processing in a structured format.
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
- [1]: `Address` objects are [address-rfc2821](https://github.com/haraka/node-address-rfc2821) objects.
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
- Of course in order to control this you may at some point need to edit some
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
- This tutorial will run through a simple plugin which allows you to have
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
- ## The Design
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][1]
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
- First install Haraka via npm if you haven't already:
16
+ Install Haraka and create a project:
40
17
 
41
- $ sudo npm -g install Haraka
18
+ ```sh
19
+ sudo npm install -g Haraka
20
+ haraka -i /path/to/new_project
21
+ ```
42
22
 
43
- Now we can create our project directory to get started with:
23
+ Use a directory that does not yet exist. Now scaffold a plugin:
44
24
 
45
- $ haraka -i /path/to/new_project
25
+ ```sh
26
+ haraka -c /path/to/new_project -p rcpt_to.disposable
27
+ ```
46
28
 
47
- Make sure you use a directory that doesn't exist for your project.
29
+ `haraka -p` reports the files it created:
48
30
 
49
- Next, let's create a new plugin:
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
- $ haraka -c /path/to/new_project -p rcpt_to.disposable
38
+ Edit `config/plugins` so the only enabled lines are:
52
39
 
53
- This should output a bunch of information about files it has created:
40
+ ```
41
+ rcpt_to.disposable
42
+ rcpt_to.in_host_list
43
+ queue/test
44
+ ```
54
45
 
55
- Plugin rcpt_to.disposable created
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
- So let's do the second part now - load up the `config/plugins` file and
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
- # default list of plugins
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
- #dns-lists
68
- #data.signatures
58
+ Verify it works. In one terminal:
69
59
 
70
- # block mail from some known bad HELOs - see config/helo.checks.ini for configuration
71
- #helo.checks
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
- # Only accept mail where the MAIL FROM domain is resolvable to an MX record
74
- #mail_from.is_resolvable
66
+ In another:
75
67
 
76
- # Allow dated tagged addresses
77
- rcpt_to.disposable
68
+ ```sh
69
+ swaks -h example.com -t booya@myserver.com -f sender@example.com -s localhost -p 25
70
+ ```
78
71
 
79
- # Only accept mail for your personal list of hosts
80
- rcpt_to.in_host_list
72
+ You should see something like this in the Haraka log:
81
73
 
82
- # Queue mail via qmail-queue
83
- #queue/qmail-queue
74
+ ```
75
+ [INFO] [<uuid>] [rcpt_to.disposable] got recipient: <booya@myserver.com>
76
+ ```
84
77
 
85
- test_queue
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
- Here we log that we got the recipient.
80
+ ## Parsing Out the Date
98
81
 
99
- Check this works. You'll need two terminal windows. In window 1:
82
+ Detect addresses of the form `user-YYYYMMDD` and parse the date:
100
83
 
101
- $ echo LOGDEBUG > config/loglevel
102
- $ echo myserver.com >> config/host_list
103
- $ sudo haraka -c /path/to/new_project
84
+ ```js
85
+ exports.hook_rcpt = (next, connection, params) => {
86
+ const rcpt = params[0]
87
+ connection.loginfo(`got recipient: ${rcpt}`)
104
88
 
105
- And in window 2:
89
+ const match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user)
90
+ if (!match) return next()
106
91
 
107
- $ swaks -h domain.com -t booya@myserver.com -f somewhere@example.com \
108
- -s localhost -p 25
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
- In the logs you should see:
96
+ next()
97
+ }
98
+ ```
111
99
 
112
- [INFO] [rcpt_to.disposable] Got recipient: <booya@myserver.com>
100
+ Restart Haraka and send:
113
101
 
114
- Which indicates everything is working. You should also have a file
115
- `/tmp/mail.eml` containing the email that swaks sent.
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
- ## Parsing Out The Date
107
+ Logs:
118
108
 
119
- Now check for emails with an expire date in them and turn them into
120
- `Date` objects. Edit your plugin file as follows:
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
- exports.hook_rcpt = function (next, connection, params) {
123
- var rcpt = params[0];
124
- this.loginfo("Got recipient: " + rcpt);
114
+ ## Rejecting Expired Addresses
125
115
 
126
- // Check user matches regex 'user-YYYYMMDD':
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
- // get date - note Date constructor takes month-1 (i.e. Dec == 11).
133
- var expiry_date = new Date(match[2], match[3]-1, match[4]);
118
+ ```js
119
+ exports.hook_rcpt = (next, connection, params) => {
120
+ const rcpt = params[0]
134
121
 
135
- this.loginfo("Email expires on: " + expiry_date);
122
+ const match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user)
123
+ if (!match) return next()
136
124
 
137
- next();
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
- Start haraka again and pass it the following email via swaks:
130
+ next()
131
+ }
132
+ ```
141
133
 
142
- $ swaks -h domain.com -t booya-20120101@myserver.com \
143
- -f somewhere@example.com -s localhost -p 25
134
+ Send mail to an expired address:
144
135
 
145
- And you should see now in the logs:
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
- [INFO] [rcpt_to.disposable] Got recipient: <booya-20120101@myserver.com>
148
- [INFO] [rcpt_to.disposable] Email expires on: Sun, 01 Jan 2012 05:00:00 GMT
141
+ The remote end sees:
149
142
 
150
- The exact time may vary depending on your timezone, but it should be obvious
151
- we now have a date object, which we can now compare to the current date.
143
+ ```
144
+ <** 550 Expired email address
145
+ ```
152
146
 
153
- ## Rejecting Expired Emails
147
+ ## Rewriting Live Addresses
154
148
 
155
- The next edit we have to do is to add in code to compare to the current date
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
- exports.hook_rcpt = function (next, connection, params) {
159
- var rcpt = params[0];
160
- this.loginfo("Got recipient: " + rcpt);
151
+ ```js
152
+ exports.hook_rcpt = (next, connection, params) => {
153
+ const rcpt = params[0]
161
154
 
162
- // Check user matches regex 'user-YYYYMMDD':
163
- var match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user);
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
- // get date - note Date constructor takes month-1 (i.e. Dec == 11).
169
- var expiry_date = new Date(match[2], match[3]-1, match[4]);
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
- And we can easily check that with swaks (remember to restart Haraka):
184
-
185
- $ swaks -h foo.com -t booya-20110101@haraka.local -f somewhere@example.com \
186
- -s localhost -p 25
187
- === Trying localhost:25...
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
- [INFO] [rcpt_to.disposable] Got recipient: <booya-20120101@haraka.local>
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
- Which indicates that we have successfully modified the email address.
171
+ ```
172
+ [INFO] [rcpt_to.disposable] rewrote recipient to: <booya@myserver.com>
173
+ ```
254
174
 
255
- # Further Reading
175
+ ## Further Reading
256
176
 
257
- There are many more features of the Haraka API to explore, including access
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
- There are two good places to read up on these. Firstly is the documentation
262
- in the Haraka "docs" directory. Start with the `Plugins.md` file, and work
263
- your way through the API from there.
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
- The second place is simply reading the source code for the plugins themselves.
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/