milter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []