-
Notifications
You must be signed in to change notification settings - Fork 20
/
ruby-milter-tutorial.rd
465 lines (331 loc) · 12.7 KB
/
ruby-milter-tutorial.rd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
# -*- rd -*-
= milter development with Ruby --- A tutorial for Ruby bindings
== About this document
This document describes how to develop a milter with Ruby
bindings for the milter library provided by milter manager.
See ((<Developer center|URL:https://www.milter.org/developers>))
abount milter protocol.
== Install
You can specify --enable-ruby-milter option to configure script if you
want to develop milter writing Ruby. You can install packages on
Debian GNU/Linux, Ubuntu and CentOS because there are deb/rpm packages
for those platforms.
Debian GNU/Linux or Ubuntu:
% sudo aptitude -V -D -y install ruby-milter-core ruby-milter-client ruby-milter-server
CentOS:
% sudo yum install -y ruby-milter-core ruby-milter-client ruby-milter-server
You can specify --enable-ruby-milter option to configure script if
there are no package in your environment.
% ./configure --enable-ruby-milter
You can confirm installed library version.
% ruby -r milter -e 'p Milter::VERSION'
[1, 8, 0]
You have succeeded to install ruby-milter if you can see version
information.
== Summary
Milter written in Ruby is followings:
require 'milter/client'
class Session < Milter::ClientSession
def initialize(context)
super(context)
# Initialize
end
def connect(host, address)
# ...
end
# Other callback definitions
end
command_line = Milter::Client::CommandLine.new
command_line.run do |client, _options|
client.register(Session)
end
Let's write the milter that can reject a mail includes specified
regular expression.
== Callbacks
Milter callback methods are called for each event. Almost events have
additional information. You can pass additional information via
callback parameters or macro. This document describes callback
parameters.
This is the list of callback methods and parameters.
: connect(host, address)
This method is called when SMTP client connects to SMTP server.
((|host|)) is hostname of SMTP client.
((|address|)) is IP Address of SMTP client.
For example, connetct from localhost.
: host
"localhost"
: address
This is a (({Milter::SocketAddress::IPv4})) object represents
(({inet:45875@[127.0.0.1]})).
: helo(fqdn)
This method is called when SMTP client sends HELO or EHLO command.
((|fqdn|)) is FQDN reported via HELO/EHLO.
For example, SMTP client sends "EHLO mail.example.com".
: fqdn
"mail.example.com"
: envelope_from(from)
This method is called when SMTP client sends MAIL command.
((|from|)) is sender mail address reported via MAIL command.
For exapme, SMTP client sends "MAIL FROM: <user@example.com>"
: from
"<user@example.com>"
: envelope_recipient(to)
This method is called when SMTP client send RCPT command.
This method is called twice if SMTP client send RCPT command twice.
((|to|)) is recipient mail address reported via RCPT command.
For example, SMTP client sends "RCPT TO: <user@example.com>"
: to
"<user@example.com>"
: data
This method is called when SMTP client sends DATA command.
: header(name, value)
This method is called N times. N is the number of headers included
in the mail.
((|name|)) is header name.
((|value|)) is header value.
For example, there is a header which is "Subject: Hello!"
: name
"Subject"
: value
"Hello!"
: end_of_header
This method is called when milter has finished processing header of
the mail.
: body(chunk)
This method is called when milter has received mail body.
This method is called only once if mail body is small enough.
This methos is called multiple times if mail body is large.
((|chunk|)) is splitted body.
For examle, if the mail body includes "Hi!", this method is called only once.
: chunk
"Hi!"
: end_of_message
This method is called when SMTP client sends "<CR><LF>.<CR><LF>"
that represents end of data.
: abort(state)
This method is called when SMTP transaction is resetted.
In particular, after end_of_message and SMTP client sends RSET.
((|state|)) is a object represents timing of calling abort.
: unknown(command)
This method is called when SMTP client sends unknown command in
milter protocol.
((|command|)) is command name.
: reset
This method is called when initialize and finish mail transaction.
((<mail transaction|URL:http://tools.ietf.org/html/rfc5321#section-3.3>))
has finished at:
* Called (({abort})) callback.
* Call (({reject})) in milter.
* Call (({temporary_failure})) in milter.
* Call (({discard})) in milter.
* Call (({accept})) in milter.
: finished
This method is called when completed milter protocol.
TODO: write about timing
== Using callbacks
We want to write the milter which will reject mails match against
specified regular expression. The regular expression matches against
subject and message body. It is necessary for us to select header
callback and body callback. Template is as following.
require 'milter/client'
class MilterRegexp < Milter::ClientSession
def initialize(context, regexp)
super(context)
@regexp = regexp
end
def header(name, value)
# ... Check subject header
end
def body(chunk)
# check chunk
end
end
command_line = Milter::Client::CommandLine.new
command_line.run do |client, _options|
# We want to reject mails include "viagra"
client.register(MilterRegexp, /viagra/i)
end
== Check subject header
First, let's check subject header.
class MilterRegexp < Milter::ClientSession
# ...
def header(name, value)
case name
when /\ASubject\z/i
if @regexp =~ value
reject
end
end
end
# ...
end
Reject mails if header name is matched "subject" and its value matches
against specified regular expression.
== Operation check
Let's try to execute this milter.
Now, your milter is as following.
require 'milter/client'
class MilterRegexp < Milter::ClientSession
def initialize(context, regexp)
super(context)
@regexp = regexp
end
def header(name, value)
case name
when /\ASubject\z/i
if @regexp =~ value
reject
end
end
end
def body(chunk)
# Check cunk
end
end
command_line = Milter::Client::CommandLine.new
command_line.run do |client, _options|
# We want to reject mails include "viagra"
client.register(MilterRegexp, /viagra/i)
end
You can execute this file as milter below command if you save this
file as "milter-regexp.rb". We add "-v" option because it is easy
to check operation.
% ruby milter-regexp.rb -v
In this case (default), milter run in foreground. You can check
operation via other terminal.
((<milter-test-server>)) is very useful to test milter.
Milter which is written in Ruby is launched on "inet:20025@localhost".
% milter-test-server -s inet:20025
status: pass
elapsed-time: 0.00254348 seconds
You can see "status: pass" in your terminal if you can connect properly.
Let's check another terminal.
[2010-08-01T05:44:34.157419Z]: [client][accept] 10:inet:55651@127.0.0.1
[2010-08-01T05:44:34.157748Z]: [1] [client][start]
[2010-08-01T05:44:34.157812Z]: [1] [reader][watch] 4
[2010-08-01T05:44:34.157839Z]: [1] [writer][watch] 5
[2010-08-01T05:44:34.158050Z]: [1] [reader] reading from io channel...
[2010-08-01T05:44:34.158140Z]: [1] [command-decoder][negotiate]
[2010-08-01T05:44:34.158485Z]: [1] [client][reply][negotiate] #<MilterOption version=<6> action=<add-headers|change-body|add-envelope-recipient|delete-envelope-recipient|change-headers|quarantine|change-envelope-from|add-envelope-recipient-with-parameters|set-symbol-list> step=<no-connect|no-helo|no-envelope-from|no-envelope-recipient|no-end-of-header|no-unknown|no-data|skip|envelope-recipient-rejected>>
[2010-08-01T05:44:34.158605Z]: [1] [client][reply][negotiate][continue]
[2010-08-01T05:44:34.158895Z]: [1] [reader] reading from io channel...
[2010-08-01T05:44:34.158970Z]: [1] [command-decoder][header] <From>=<<kou+send@example.com>>
[2010-08-01T05:44:34.159092Z]: [1] [client][reply][header][continue]
[2010-08-01T05:44:34.159207Z]: [1] [reader] reading from io channel...
[2010-08-01T05:44:34.159269Z]: [1] [command-decoder][header] <To>=<<kou+receive@example.com>>
[2010-08-01T05:44:34.159373Z]: [1] [client][reply][header][continue]
[2010-08-01T05:44:34.159485Z]: [1] [reader] reading from io channel...
[2010-08-01T05:44:34.159544Z]: [1] [command-decoder][body] <71>
[2010-08-01T05:44:34.159656Z]: [1] [client][reply][body][continue]
[2010-08-01T05:44:34.159774Z]: [1] [reader] reading from io channel...
[2010-08-01T05:44:34.159842Z]: [1] [command-decoder][define-macro] <E>
[2010-08-01T05:44:34.159882Z]: [1] [command-decoder][end-of-message] <0>
[2010-08-01T05:44:34.159941Z]: [1] [client][reply][end-of-message][continue]
[2010-08-01T05:44:34.160034Z]: [1] [command-decoder][quit]
[2010-08-01T05:44:34.160081Z]: [1] [agent][shutdown]
[2010-08-01T05:44:34.160118Z]: [1] [agent][shutdown][reader]
[2010-08-01T05:44:34.160162Z]: [1] [reader][eof]
[2010-08-01T05:44:34.160199Z]: [1] [reader] shutdown requested.
[2010-08-01T05:44:34.160231Z]: [1] [reader] removing reader watcher.
[2010-08-01T05:44:34.160299Z]: [1] [writer][shutdown]
[2010-08-01T05:44:34.160393Z]: [0] [reader][dispose]
[2010-08-01T05:44:34.160452Z]: [client][finisher][run]
[2010-08-01T05:44:34.160492Z]: [1] [client][finish]
[2010-08-01T05:44:34.160536Z]: [1] [client][rest] []
[2010-08-01T05:44:34.160578Z]: [sessions][finished] 1(+1) 0
You cannot connect to milter if you can see nothing in this terminal.
Please check to launch milter or to specify correct address to
milter-test-server.
Let's check operation when milter process a mail included "viagra" in subject.
You can reproduce the mail as following command.
% milter-test-server -s inet:20025 --header 'Subject:Buy viagra!!!'
status: reject
elapsed-time: 0.00144477 seconds
You can check expected result because you can see "status: reject" in
you terminal.
In another terminal, you can see log as followings.
...
[2010-08-01T05:49:49.275257Z]: [2] [command-decoder][header] <Subject>=<Buy viagra!!!>
[2010-08-01T05:49:49.275405Z]: [2] [client][reply][header][reject]
...
The mitler reject the mail when process subject header.
milter manager provides usuful tools and libraries.
== Check message body
Let's check message body.
class MilterRegexp < Milter::ClientSession
def body(chunk)
if @regexp =~ chunk
reject
end
end
end
Reject mails if message body chunk matches against specified regular
expression.
Let's try. milter-test-server can specify message body via "--body"
option.
% tool/milter-test-server -s inet:20025 --body 'Buy viagra!!!'
status: reject
elapsed-time: 0.00195496 seconds
It is expected result because you can see "status: reject" in your
terminal.
== Problems
There are some problems in this milter because this milter simplify
for this tutorial.
(1) Include MIME encoded header.
Decoded "=?ISO-2022-JP?B?GyRCJVAlJCUiJTAlaRsoQnZpYWdyYQ==?="
includes "viagra", but original header value does not match
against specified regular expression. And milter does not reject
this mail
(2) The word splitted by chunk in message body.
For exapmle, Specified regular expression does not match if
first chunk has "via" and second one has "gra". And milter does
not reject this mail.
You can solve problems about header if you use NKF library as following.
require 'nkf'
class MilterRegexp < Milter::ClientSession
# ...
def header(name, value)
case name
when /\ASubject\z/i
if @regexp =~ NKF.nkf("-w", value)
reject
end
end
end
# ...
end
You can solve problems about message body if milter check message body
when milter receives all chunks.
class MilterRegexp < Milter::ClientSession
...
def initialize(context, regexp)
super(context)
@regexp = regexp
@body = ""
end
def body(chunk)
if @regexp =~ chunk
reject
end
@body << chunk
end
def end_of_mesasge
if @regexp =~ @body
reject
end
end
...
end
You can test multiple chunks as following.
% milter-test-server -s inet:20025 --body 'Buy via' --body 'gra!!!'
status: reject
elapsed-time: 0.00379063 seconds
Mails are rejected if it includes multiple chunks.
However, in this case, all messages place on memory. This is
performance problem. In addition, this milter does not work if message
body is BASE64 encoded.
((<Mail|URL:http://github.com/mikel/mail>)) is useful library that
handle mails.
== Conclusion
This document describes how to write milter in Ruby. It is easy to
write milter in Ruby. Let's try to write milter in Ruby!!