| 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__ |