milter 0.0.1

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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in milter.gemspec
4
+ gemspec
@@ -0,0 +1,29 @@
1
+ # Milter
2
+
3
+ This software originated as a clone of ppymilter (http://code.google.com/p/ppymilter/) by Hirohisa Mitsuishi (https://github.com/bongole).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'milter'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install milter
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,18 @@
1
+ require 'milter'
2
+
3
+ # example
4
+ # change mail body
5
+ class MyMilter < Milter::Milter
6
+ def header( k,v )
7
+ puts "#{k} => #{v}"
8
+ return Response.continue
9
+ end
10
+
11
+ def end_body( *args )
12
+ puts "BODY => #{@body}"
13
+ return [Response.replace_body("hogehoge"), Response.continue]
14
+ end
15
+ end
16
+
17
+ Milter.register(MyMilter)
18
+ Milter.start
@@ -0,0 +1,70 @@
1
+ require 'milter'
2
+
3
+ class MyMilter < Milter::Milter
4
+
5
+ def connect( hostname, family, port, address )
6
+ puts "connect from #{address}:#{port} (#{hostname})."
7
+ return Response.continue
8
+ end
9
+
10
+ def helo( text )
11
+ puts "helo #{text}"
12
+ return Response.continue
13
+ end
14
+
15
+ def mail_from( mailfrom, esmtp_info )
16
+ puts "mail_from: #{mailfrom}"
17
+ return Response.continue
18
+ end
19
+
20
+ def rcpt_to( mailto, esmtp_info )
21
+ puts "rcpt_to: #{mailto} (#{esmtp_info})"
22
+ return Response.continue
23
+ end
24
+
25
+ def data
26
+ puts "data"
27
+ return Response.continue
28
+ end
29
+
30
+ def unknown( data )
31
+ puts "unknown smtp command: #{data}"
32
+ return Response.continue
33
+ end
34
+
35
+ def header( k, v )
36
+ puts "header: #{k} => #{v}"
37
+ @headers[k] = [] if @headers[k].nil?
38
+ @headers[k] << v
39
+ return Response.continue
40
+ end
41
+
42
+ def end_headers
43
+ puts "end of headers; headers: #{@headers.inspect}"
44
+ return Response.continue
45
+ end
46
+
47
+ def end_body( data )
48
+ puts "end of body: #{data}; body: #{@body}"
49
+ return Response.continue
50
+ end
51
+
52
+ def abort
53
+ puts "abort"
54
+ return Response.continue
55
+ end
56
+
57
+ def quit
58
+ puts "quit"
59
+ return Response.continue
60
+ end
61
+
62
+ def macro( cmd, data )
63
+ puts "#{Milter::COMMANDS[cmd]} macros: #{data.inspect}"
64
+ return Response.continue
65
+ end
66
+
67
+ end
68
+
69
+ Milter.register(MyMilter)
70
+ Milter.start
@@ -0,0 +1,330 @@
1
+ require 'milter/version'
2
+ require 'eventmachine'
3
+
4
+ module Milter
5
+ SMFI_PROT_VERSION = 6 # "MTA - libmilter protocol version"
6
+ MILTER_LEN_BYTES = 4 # "length of 32 bit integer in bytes"
7
+
8
+ # Milter binary protocol commands
9
+ SMFIC_ABORT = 'A' # "Abort"
10
+ SMFIC_BODY = 'B' # "Body chunk"
11
+ SMFIC_CONNECT = 'C' # "Connection information"
12
+ SMFIC_MACRO = 'D' # "Define macro"
13
+ SMFIC_BODYEOB = 'E' # "final body chunk (End)"
14
+ SMFIC_HELO = 'H' # "HELO/EHLO"
15
+ SMFIC_QUIT_NC = 'K' # "QUIT but new connection follows"
16
+ SMFIC_HEADER = 'L' # "Header"
17
+ SMFIC_MAIL = 'M' # "MAIL from"
18
+ SMFIC_EOH = 'N' # "EOH"
19
+ SMFIC_OPTNEG = 'O' # "Option negotation"
20
+ SMFIC_QUIT = 'Q' # "QUIT"
21
+ SMFIC_RCPT = 'R' # "RCPT to"
22
+ SMFIC_DATA = 'T' # "DATA"
23
+ SMFIC_UNKNOWN = 'U' # "Any unknown command"
24
+
25
+ # mappings for Ruby callbacks
26
+ COMMANDS = {
27
+ SMFIC_ABORT => :abort,
28
+ SMFIC_BODY => :body,
29
+ SMFIC_CONNECT => :connect,
30
+ SMFIC_MACRO => :macro,
31
+ SMFIC_BODYEOB => :end_body,
32
+ SMFIC_HELO => :helo,
33
+ SMFIC_QUIT_NC => :quit_new_connection,
34
+ SMFIC_HEADER => :header,
35
+ SMFIC_MAIL => :mail_from,
36
+ SMFIC_EOH => :end_headers,
37
+ SMFIC_OPTNEG => :opt_neg,
38
+ SMFIC_QUIT => :quit,
39
+ SMFIC_RCPT => :rcpt_to,
40
+ SMFIC_DATA => :data,
41
+ SMFIC_UNKNOWN => :unknown,
42
+ }
43
+
44
+ # actions(replies)
45
+ RESPONSE = {
46
+ :addrcpt => '+', # SMFIR_ADDRCPT # "add recipient"
47
+ :delrcpt => '-', # SMFIR_DELRCPT # "remove recipient"
48
+ :addrcpt_par => '2', # SMFIR_ADDRCPT_PAR # "add recipient (incl. ESMTP args)"
49
+ :shutdown => '4', # SMFIR_SHUTDOWN # "421: shutdown (internal to MTA)"
50
+ :accept => 'a', # SMFIR_ACCEPT # "accept"
51
+ :replbody => 'b', # SMFIR_REPLBODY # "replace body (chunk)"
52
+ :continue => 'c', # SMFIR_CONTINUE # "continue"
53
+ :discard => 'd', # SMFIR_DISCARD # "discard"
54
+ :chgfrom => 'e', # SMFIR_CHGFROM # "change envelope sender (from)"
55
+ :connfail => 'f', # SMFIR_CONN_FAIL # "cause a connection failure"
56
+ :addheader => 'h', # SMFIR_ADDHEADER # "add header"
57
+ :insheader => 'i', # SMFIR_INSHEADER # "insert header"
58
+ :setsymlist => 'l', # SMFIR_SETSYMLIST # "set list of symbols (macros)"
59
+ :chgheader => 'm', # SMFIR_CHGHEADER # "change header"
60
+ :progress => 'p', # SMFIR_PROGRESS # "progress"
61
+ :quarantine => 'q', # SMFIR_QUARANTINE # "quarantine"
62
+ :reject => 'r', # SMFIR_REJECT # "reject"
63
+ :skip => 's', # SMFIR_SKIP # "skip"
64
+ :tempfail => 't', # SMFIR_TEMPFAIL # "tempfail"
65
+ :replycode => 'y', # SMFIR_REPLYCODE # "reply code etc"
66
+ }
67
+
68
+ # What the MTA can send/filter wants in protocol
69
+ PROTOCOL_FLAGS = {
70
+ :noconnect => 0x000001, # SMFIP_NOCONNECT # MTA should not send connect info
71
+ :nohelo => 0x000002, # SMFIP_NOHELO # MTA should not send HELO info
72
+ :nomail => 0x000004, # SMFIP_NOMAIL # MTA should not send MAIL info
73
+ :norcpt => 0x000008, # SMFIP_NORCPT # MTA should not send RCPT info
74
+ :nobody => 0x000010, # SMFIP_NOBODY # MTA should not send body
75
+ :nohdrs => 0x000020, # SMFIP_NOHDRS # MTA should not send headers
76
+ :noeoh => 0x000040, # SMFIP_NOEOH # MTA should not send EOH
77
+ :nohrepl => 0x000080, # SMFIP_NR_HDR # No reply for headers
78
+ :nounknown => 0x000100, # SMFIP_NOUNKNOWN # MTA should not send unknown commands
79
+ :nodata => 0x000200, # SMFIP_NODATA # MTA should not send DATA
80
+ :skip => 0x000400, # SMFIP_SKIP # MTA understands SMFIS_SKIP
81
+ :rcpt_rej => 0x000800, # SMFIP_RCPT_REJ # MTA should also send rejected RCPTs
82
+ :nr_conn => 0x001000, # SMFIP_NR_CONN # No reply for connect
83
+ :nr_helo => 0x002000, # SMFIP_NR_HELO # No reply for HELO
84
+ :nr_mail => 0x004000, # SMFIP_NR_MAIL # No reply for MAIL
85
+ :nr_rcpt => 0x008000, # SMFIP_NR_RCPT # No reply for RCPT
86
+ :nr_data => 0x010000, # SMFIP_NR_DATA # No reply for DATA
87
+ :nr_unkn => 0x020000, # SMFIP_NR_UNKN # No reply for UNKN
88
+ :nr_eoh => 0x040000, # SMFIP_NR_EOH # No reply for eoh
89
+ :nr_body => 0x080000, # SMFIP_NR_BODY # No reply for body chunk
90
+ :hrd_leadspc => 0x100000, # SMFIP_HDR_LEADSPC # header value leading space
91
+ }
92
+
93
+ # What the filter might do -- values to be ORed together
94
+ # (from mfapi.h)
95
+ ACTION_FLAGS = {
96
+ :none => 0x000, # SMFIF_NONE # no flags
97
+ :addhdrs => 0x001, # SMFIF_ADDHDRS # filter may add headers
98
+ :chgbody => 0x002, # SMFIF_CHGBODY # filter may replace body
99
+ :addrcpt => 0x004, # SMFIF_ADDRCPT # filter may add recipients
100
+ :delrcpt => 0x008, # SMFIF_DELRCPT # filter may delete recipients
101
+ :chghdrs => 0x010, # SMFIF_CHGHDRS # filter may change/delete headers
102
+ :quarantine => 0x020, # SMFIF_QUARANTINE # filter may quarantine envelope
103
+ :chgfrom => 0x040, # SMFIF_CHGFROM # filter may change "from" (envelope sender)
104
+ :addrcpt_par => 0x080, # SMFIF_ADDRCPT_PAR # add recipients incl. args
105
+ :setsymlist => 0x100, # SMFIF_SETSYMLIST # filter can send set of symbols (macros) that it wants
106
+ :set_curr => 0x1FF, # SMFI_CURR_ACTS # Set of all actions in the current milter version
107
+ }
108
+
109
+ class Milter
110
+ def initialize
111
+ @body = ''
112
+ @headers = {}
113
+ @recipients = []
114
+ end
115
+
116
+ def opt_neg( ver, actions, protocol )
117
+ puts "New Milter connection - version: %s, action flags: %x, protocol flags: %x." % [ver, actions, protocol]
118
+ _actions = ACTION_FLAGS[:set_curr] # allow all supported actions
119
+ # _actions = ACTION_FLAGS.values_at(:addhrds, :chgfrom).reduce(:+) # example for enabling individual actions
120
+ _protocol = PROTOCOL_FLAGS.values_at(:rcpt_rej).reduce(:+) # register callback for rejected recipients
121
+ return SMFIC_OPTNEG + [ SMFI_PROT_VERSION, _actions, _protocol].pack("NNN")
122
+ end
123
+
124
+ def rcpt_to( mailto, esmtp_info )
125
+ @recipients << mailto
126
+ return Response.continue
127
+ end
128
+
129
+ def header( k,v )
130
+ @headers[k] = [] if @headers[k].nil?
131
+ @headers[k] << v
132
+ return Response.continue
133
+ end
134
+
135
+ def body( data )
136
+ @body << data
137
+ return Response.continue
138
+ end
139
+
140
+ class Response
141
+ class << self
142
+ def continue
143
+ RESPONSE[:continue]
144
+ end
145
+
146
+ def reject
147
+ RESPONSE[:reject]
148
+ end
149
+
150
+ def accept
151
+ RESPONSE[:accept]
152
+ end
153
+
154
+ #email must be enclosed in <>
155
+ def delete_rcpt( email )
156
+ RESPONSE[:delrcpt] + email + "\0"
157
+ end
158
+
159
+ #email must be enclosed in <>
160
+ def add_rcpt( email )
161
+ RESPONSE[:addrcpt] + email + "\0"
162
+ end
163
+
164
+ #index is for multiple occurences of same header (starts at 1)
165
+ def change_header( header, value, index=1 )
166
+ index = [index].pack('N')
167
+ RESPONSE[:chgheader] + "#{index}#{header}\0#{value}" + "\0"
168
+ end
169
+
170
+ def replace_body( body )
171
+ RESPONSE[:replbody] + body + "\0"
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ class MilterConnectionHandler < EM::Connection
178
+ @@milter_class = Milter
179
+
180
+ def initialize
181
+ @data = ''
182
+ @milter = @@milter_class.new
183
+ end
184
+
185
+ def send_milter_response( res )
186
+ r = [ res.size ].pack('N') + res
187
+ send_data(r)
188
+ end
189
+
190
+ # SMFIC_OPTNEG, two integers
191
+ def parse_opt_neg( data )
192
+ ver, actions, protocol = data.unpack('NNN')
193
+ return [ver, actions, protocol]
194
+ end
195
+
196
+ # SMFIC_MACRO, \0 separated list of args, NULL-terminated
197
+ def parse_macro( data )
198
+ cmd, macros = data[0].chr, data[1..-1].split("\0" )
199
+ return [cmd, Hash[*macros]]
200
+ end
201
+
202
+ # SMFIC_CONNECT, two args (strings)
203
+ def parse_connect( data )
204
+ hostname, val = data.split("\0", 2)
205
+ family = val[0].unpack('C')
206
+ port = val[1...3].unpack('n')
207
+ address = val[3..-1]
208
+ return [hostname, family, port, address]
209
+ end
210
+
211
+ # SMFIC_HELO, one arg (string)
212
+ def parse_helo( data )
213
+ return [data]
214
+ end
215
+
216
+ # SMFIC_MAIL, \0 separated list of args, NULL-terminated
217
+ def parse_mail_from( data )
218
+ mailfrom, esmtp_info = data.split("\0", 2 )
219
+ return [mailfrom, esmtp_info.split("\0")]
220
+ end
221
+
222
+ # SMFIC_RCPT, \0 separated list of args, NULL-terminated
223
+ def parse_rcpt_to( data )
224
+ mailto, esmtp_info = data.split("\0", 2 )
225
+ return [mailto, esmtp_info.split("\0")]
226
+ end
227
+
228
+ # SMFIC_HEADER, two args (strings)
229
+ def parse_header( data )
230
+ k,v = data.split("\0", 2)
231
+ return [k, v.delete("\0")]
232
+ end
233
+
234
+ # SMFIC_EOH, no args
235
+ def parse_end_headers( data )
236
+ return []
237
+ end
238
+
239
+ # SMFIC_BODY, one arg (string)
240
+ def parse_body( data )
241
+ return [ data.delete("\0") ]
242
+ end
243
+
244
+ # SMFIC_BODYEOB, one arg (string)
245
+ def parse_end_body( data )
246
+ return [data]
247
+ end
248
+
249
+ # SMFIC_QUIT, no args
250
+ def prase_quit( data )
251
+ return []
252
+ end
253
+
254
+ # SMFIC_ABORT, no args
255
+ def parse_abort( data )
256
+ return []
257
+ end
258
+
259
+ # SMFIC_DATA, no args
260
+ def parse_data( data )
261
+ return []
262
+ end
263
+
264
+ # SMFIC_UNKNOWN, one arg (string)
265
+ def parse_unknown( data )
266
+ return data
267
+ end
268
+
269
+ # SMFIC_QUIT_NC, no args
270
+ def parse_quit_new_connection( data )
271
+ return []
272
+ end
273
+
274
+ def receive_data( data )
275
+ # puts "Data: #{data.bytes.map(&:chr)}"
276
+ @data << data
277
+ while @data.size >= MILTER_LEN_BYTES
278
+ pkt_len = @data[0...MILTER_LEN_BYTES].unpack('N').first
279
+ if @data.size >= MILTER_LEN_BYTES + pkt_len
280
+ @data.slice!(0, MILTER_LEN_BYTES)
281
+ pkt = @data.slice!(0, pkt_len)
282
+ cmd, val = pkt[0].chr, pkt[1..-1]
283
+
284
+ if COMMANDS.include?(cmd) and @milter.respond_to?(COMMANDS[cmd])
285
+ method_name = COMMANDS[cmd]
286
+ args = []
287
+ args = self.send('parse_' + method_name.to_s, val ) if self.respond_to?('parse_' + method_name.to_s )
288
+ ret = @milter.send(method_name, *args )
289
+
290
+ close_connection and return if cmd == SMFIC_QUIT
291
+ next if cmd == SMFIC_MACRO
292
+
293
+ if not ret.is_a? Array
294
+ ret = [ ret ]
295
+ end
296
+
297
+ ret.each do |r|
298
+ send_milter_response(r)
299
+ end
300
+ else
301
+ next if cmd == SMFIC_MACRO
302
+ send_milter_response(RESPONSE[:continue])
303
+ end
304
+ else
305
+ break
306
+ end
307
+ end
308
+ end
309
+
310
+ class << self
311
+ def register( milter_class )
312
+ @@milter_class = milter_class
313
+ end
314
+ end
315
+ end
316
+
317
+ class << self
318
+ def register( milter_class )
319
+ MilterConnectionHandler.register(milter_class)
320
+ end
321
+
322
+ def start( host = 'localhost', port = 8888 )
323
+ EM.run do
324
+ Signal.trap("INT") { EventMachine.stop }
325
+ Signal.trap("TERM") { EventMachine.stop }
326
+ EM.start_server host, port, MilterConnectionHandler
327
+ end
328
+ end
329
+ end
330
+ end
@@ -0,0 +1,3 @@
1
+ module Milter
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'milter/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "milter"
8
+ spec.version = Milter::VERSION
9
+ spec.authors = ["Markus Strauss"]
10
+ spec.email = ["Markus@ITstrauss.eu"]
11
+ # spec.description = %q{A pure Ruby Milter library}
12
+ spec.summary = %q{A pure Ruby Milter library}
13
+ spec.homepage = "https://github.com/mstrauss/ruby-milter.git"
14
+ # spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_dependency "eventmachine", "~> 1.0"
24
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: milter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Markus Strauss
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: eventmachine
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ description:
63
+ email:
64
+ - Markus@ITstrauss.eu
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - Gemfile.lock
72
+ - README.md
73
+ - Rakefile
74
+ - examples/body_filter.rb
75
+ - examples/logging.rb
76
+ - lib/milter.rb
77
+ - lib/milter/version.rb
78
+ - milter.gemspec
79
+ homepage: https://github.com/mstrauss/ruby-milter.git
80
+ licenses: []
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ segments:
92
+ - 0
93
+ hash: -2274297183430300128
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ segments:
101
+ - 0
102
+ hash: -2274297183430300128
103
+ requirements: []
104
+ rubyforge_project:
105
+ rubygems_version: 1.8.25
106
+ signing_key:
107
+ specification_version: 3
108
+ summary: A pure Ruby Milter library
109
+ test_files: []