1
=begin
2
3
encrypt_smtp_server
4
5
Invocation: ruby gpg_passphrase [configfile]
6
7
Setup a simple SMTP server for filtering message stream for GPG encryption
8
9
If public keys for all recipients in the message stream are available, the
10
message is encrypted. Otherwise a separate signature is generated
11
12
(C) 2009, GNU General Public Licence, Author: Otto Linnemann
13
14
=end
15
16
require 'smtp_tls'
17
# require 'net/smtp'
18
require 'socket'
19
require 'stringio'
20
require 'set'
21
22
require 'plist_parser'
23
require 'gpgmime'
24
25
class SmtpServer
26
27
  LINESEP = "\r\n"
28
  MULTICLIENTS  = true  # if true more than one process is listening to the input port
29
  
30
  # Initialization of SMTP Filter GPG/MIME encryption and decryption add on
31
  # The parameters are normally retrieved from a configuration file and passphrase dialoges
32
  def initialize( 
33
    ext_server, 
34
    ext_port, 
35
    ext_account, 
36
    ext_passphrase, 
37
    from_address, 
38
    authtype,
39
    local_port, 
40
    gpg_passphrase, 
41
    logfilename = File.join( ENV["HOME"], "/Library/Logs/GpgServer.log" )
42
    )
43
    @mailServerName       = ext_server
44
    @mailServerPort       = ext_port
45
    @helo                 = 'localhost.localdomain'
46
    @account              = ext_account
47
    @from_address         = from_address
48
    @smtp_passphrase      = ext_passphrase
49
    @authtype             = authtype
50
    @gpg_passphrase       = gpg_passphrase
51
    @local_port           = local_port
52
    @log                  = File.new( logfilename, "w" )
53
  end
54
  
55
  attr_accessor :gpg_passphrase, :smtp_passphrase, :authtype, :from_address, :account, :helo, :mailServerPort, :mailServerName, :local_port
56
  
57
  
58
  private
59
  
60
  def log( string )
61
    if @log
62
      @log.puts string
63
      @log.flush
64
    end
65
  end
66
  
67
  
68
  # sends mail via specified SMTP server
69
  def sendmail( rcpArray, message )
70
    # File.open("mimeparser_out.eml", "w") { |stream| stream.write( message ) }
71
72
    Net::SMTP.start( @mailServerName, @mailServerPort, @helo, @account, @smtp_passphrase, @authtype ) do |smtp|
73
    	smtp.send_message( message, @from_address, rcpArray )
74
    end
75
  end
76
  
77
    
78
  # gpg encryption filter for content
79
  # returns encrypted_msg, pgpinfo
80
  def gpgfilter( rcpArray, content_string )
81
    
82
    # log "gpgpassphrase "+ @gpg_passphrase
83
    
84
    mimeParser = GpgMime.new
85
    mimeParser.setLogStream( @log )
86
    
87
    input_stream = StringIO.open( content_string )
88
    mimeParser.read_next_message( input_stream )
89
    pubKeysArray = mimeParser.getPubKeyAddressList
90
91
    rcpSet      = rcpArray.map { |s| s.downcase }.to_set
92
    pubKeysSet  = pubKeysArray.map { |s| s.downcase }.to_set
93
    interSec = rcpSet & pubKeysSet
94
    
95
    if interSec.length < rcpSet.length
96
      log "not all in key list"
97
      gpgmsg = mimeParser.clearsign( @gpg_passphrase ) 
98
    else
99
      log "everything in key list, encrypt"
100
      log "rcpArry-> "
101
      rcpArray.each { |r| log r }
102
      gpgmsg = mimeParser.encrypt_and_sign( @gpg_passphrase, rcpArray )
103
    end
104
    
105
    log "gpgmsg: " + gpgmsg if gpgmsg.length > 0
106
    
107
    [mimeParser.getMessage, gpgmsg ]
108
  end
109
110
111
  public
112
113
  def handle_client( client )
114
    
115
    state = :reg_ack
116
    clientname = ""
117
    from_addr = ""
118
    to_addr = ""
119
    data = ""
120
    content = ""
121
    
122
 
123
    line = ""
124
    rcpArray = []
125
    
126
    log "Client is connected, wait for messages"
127
    client.write "220 localhost GPG SMTP filter"+LINESEP   
128
       
129
    begin # rescue exception of this block
130
         
131
      loop do
132
133
        # State loop
134
135
        line = client.gets
136
      
137
        if state!=:data_rcv && line.upcase.match(/^QUIT/) then
138
          log "quit message: " + line
139
          client.write "221 Bye" + LINESEP
140
          client.close
141
          log "Client is disconnected"       
142
          break 
143
        end
144
      
145
        if state!=:data_rcv && line.upcase.match(/^NOOP/) then next end
146
        if state!=:data_rcv && line.upcase.match(/^RSET/) then break end
147
      
148
        lineprocessed = true
149
        
150
        begin  # continue loop
151
        
152
          case state
153
            when :reg_ack
154
              log ":reg_ack"
155
              clientname = line
156
              clientname = clientname.chomp
157
              log "Connect with client: " + clientname 
158
              if clientname.upcase.match("GPGDECODE")
159
                client.write "two arguments expected: gpg_mime_encrypted_file gpg_mime_decrypted_file"+LINESEP
160
                state = :gpg_decode_ack
161
              else
162
                client.write "250 8BITMIME" + LINESEP
163
                state = :sender_ack
164
              end
165
              
166
            when :gpg_decode_ack
167
              begin
168
                if line.match('"')
169
                  # file arguments are quoted due to spaces
170
                  line.match(/(["][^"]+["])[ ]+(["][^"]+["])/)
171
                  filearray = [ Regexp.last_match(1), Regexp.last_match(2) ]
172
                  filearray.each { |a| a.gsub!('"', '') }
173
                else
174
                  # unquoted file arguments
175
                  filearray = line.split
176
                end
177
            
178
                gpg_encrypted = filearray[0]
179
                gpg_target = filearray[1]                
180
              rescue
181
                raise "error in file specifiers, wrong quotation?"
182
              end
183
              
184
              # do the decryption
185
              gpgmsg = ""
186
              begin
187
                mimeParser = GpgMime.new
188
                File.open(gpg_encrypted, "r") { |s| mimeParser.read_next_message( s ) }
189
                gpgmsg = mimeParser.decode( @gpg_passphrase )
190
                File.open(gpg_target, "w") { |s| s.write( mimeParser.getMessage ) }
191
              rescue => exception 
192
                gpgmsg = exception.to_s
193
              end 
194
              # if it was ok.
195
              if gpgmsg.length == 0
196
                msg = "250 Decryption: OK"
197
                client.write msg + LINESEP
198
                log msg + ", EML file " + gpg_encrypted + " and store decoded text to file " + gpg_target
199
              else
200
                msg = "600 Decryption Error: " + gpgmsg
201
                client.write msg + LINESEP
202
                log msg
203
              end
204
              state =:reg_ack
205
            
206
            when :sender_ack
207
              from_addr = line
208
              log "from address: "+from_addr      
209
              client.puts "250 Sender OK" + LINESEP
210
              rcpArray = []
211
              content = ""
212
              state = :recipient_ack
213
    
214
            when :recipient_ack
215
              if line.upcase.match( "RCPT TO:" ) 
216
                /[\.A-Za-z_-]+@[\.A-Za-z_-]+/ =~ line
217
                rcpArray << $~.to_s
218
                log "to address: "+ $~.to_s
219
                client.write "250 Recipient OK" + LINESEP
220
              else              
221
                state = :data_ack
222
                lineprocessed = false
223
              end
224
            
225
            when :data_ack
226
              log "data message: " + line
227
              client.write "354 End data with ." + LINESEP
228
              lineprocessed = true
229
              state = :data_rcv
230
    
231
            when :data_rcv
232
              if line == "."+LINESEP
233
                log "end of message received"
234
          
235
                encrypted_msg, gpginfo = gpgfilter(rcpArray, content)
236
          
237
=begin
238
                # log files for debugging purposes 
239
                clear = File.new("clear.eml", "w")
240
                clear.write( content )
241
                clear.close
242
              
243
                enc = File.new("enc.eml", "w")
244
                enc.write( encrypted_msg )
245
                enc.close
246
=end
247
248
                if gpginfo.length == 0
249
                  sendmail( rcpArray, encrypted_msg )  
250
                  client.write "250 OK" + LINESEP
251
                  log "mail sent, 250 OK signaled, last line received: " + line
252
                else
253
                  log "encryption error occured!"
254
                  client.write "211 " + gpginfo + LINESEP
255
                end
256
                state = :sender_ack   # Apple mail does not respectively late send quit, therefore disconnect here
257
              else 
258
                content << line
259
                # log "line: " + line
260
              end
261
                
262
          end # case state 
263
        
264
        end until lineprocessed # repeat loop
265
266
      end # state loop
267
    
268
    rescue => exception
269
      log "exception occured while sending mail out: " + exception.to_s + ", check your smtp configuration!"
270
    ensure
271
      client.close
272
    end
273
    
274
  end #def handle_client( client )
275
276
277
  # starts smtp server
278
  def run
279
280
    server = TCPServer.open( @local_port )
281
    log "Server is up, waiting for clients ..."
282
    
283
    loop {
284
      if MULTICLIENTS
285
        Thread.start(server.accept) do |client|
286
          log "New client is connected"
287
          handle_client(client)
288
          # sleep(3)
289
        end
290
      else
291
        # Connection loop 
292
        client = server.accept
293
        log "New client is connected"
294
        handle_client( client )
295
      end
296
    }
297
298
  end # def run
299
300
end # class SmtpServer
301
302
303
# ------------- Main script  ------------- 
304
# Invocation: ruby gpg_passphrase [configfile]
305
306
307
# Read configuration file 
308
309
if ARGV[1] == nil
310
  # in case no config file is given use default (here for MacOS X)
311
  configfilename = File.join( ENV["HOME"], "/Library/Preferences/GpgServer.plist" )
312
else
313
  configfilename = ARGV[1]
314
end
315
316
xml = ""
317
File.open( configfilename, "r") { |stream| xml = stream.read}
318
config  = PropertyParser.new.parse( xml )
319
320
extSmtp=config[0]["External SMTP"]
321
local=config[0]["Local SMTP"]
322
323
324
# Initialize and start up the SMTP GPG/MIME Filter-Server
325
326
myServer = SmtpServer.new(
327
  extSmtp["Server"],
328
  extSmtp["Port"],
329
  extSmtp["Account"],
330
  extSmtp["Passphrase"],
331
  extSmtp["FromAddress"],
332
  extSmtp["Authentification Type"],
333
  local["Port"],
334
  ARGV[0],  # gpg passphrase 
335
  logfilename = File.join( ENV["HOME"], "/Library/Logs/GpgServer.log" )
336
  )
337
338
339
# Check Passphrase and run the server
340
341
result = 0
342
if GpgMime.new.check_passphrase( ARGV[0] )
343
  myServer.run
344
  result = 0
345
else
346
  result = -1
347
end
348
349
exit result
350
351
352
__END__