-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmail.py
executable file
·390 lines (338 loc) · 13.2 KB
/
mail.py
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
#!/usr/bin/python
# coding: utf8
import os
try:
version = ('git ' + open(
os.path.join(os.path.dirname(__file__), '.git/refs/heads/master')
).read().strip())
except Exception:
version = 'unknown'
import email
import signal
import smtplib
import sys
import time
import traceback
import xmpp
import logging
from xmpp.browser import *
from email.MIMEText import MIMEText
from email.Header import decode_header
import config
import xmlconfig
_log = logging.getLogger(__name__)
def get_html2text(config):
try:
import html2text as htmod
except Exception, exc:
_log.warning("html2text import failure: %r", exc)
return lambda s: s # dummy replacement
## TODO: use a newer version of html2text and the values from config.
htmod.LINKS_EACH_PARAGRAPH = 1
htmod.SKIP_INTERNAL_LINKS = 1
# htmod.UNICODE_SNOB = 0 # default
return htmod.html2text
def config_logging(config):
## TODO: use some actual config stuff
logging.basicConfig(level=1)
class Transport:
online = 1
restart = 0
offlinemsg = ''
def __init__(self, jabber):
self.jabber = jabber
self.watchdir = os.path.expanduser(config.watchDir)
# A list of two element lists, 1st is xmpp domain, 2nd is email domain
self.mappings = [mapping.split('=') for mapping in config.domains]
self.jto_fallback = config.fallbackToJid
email.Charset.add_charset('utf-8', email.Charset.SHORTEST, None, None)
def register_handlers(self):
self.jabber.RegisterHandler('message', self.xmpp_message)
self.jabber.RegisterHandler('presence', self.xmpp_presence)
self.disco = Browser()
self.disco.PlugIn(self.jabber)
self.disco.setDiscoHandler(self.xmpp_base_disco, node='',
jid=config.jid)
# Disco Handlers
def xmpp_base_disco(self, con, event, ev_type):
fromjid = event.getFrom().__str__()
to = event.getTo()
node = event.getQuerynode()
#Type is either 'info' or 'items'
if to == config.jid:
if node == None:
if ev_type == 'info':
return dict(
ids=[dict(category='gateway', type='smtp',
name=config.discoName)],
features=[NS_VERSION, NS_COMMANDS])
if ev_type == 'items':
return []
else:
self.jabber.send(Error(event, ERR_ITEM_NOT_FOUND))
raise NodeProcessed
else:
self.jabber.send(Error(event, MALFORMED_JID))
raise NodeProcessed
#XMPP Handlers
def xmpp_presence(self, con, event):
# Add ACL support
fromjid = event.getFrom()
ev_type = event.getType()
to = event.getTo()
if ev_type == 'subscribe':
self.jabber.send(Presence(to=fromjid, frm = to, typ = 'subscribe'))
elif ev_type == 'subscribed':
self.jabber.send(Presence(to=fromjid, frm = to, typ = 'subscribed'))
elif ev_type == 'unsubscribe':
self.jabber.send(Presence(to=fromjid, frm = to, typ = 'unsubscribe'))
elif ev_type == 'unsubscribed':
self.jabber.send(Presence(to=fromjid, frm = to, typ = 'unsubscribed'))
elif ev_type == 'probe':
self.jabber.send(Presence(to=fromjid, frm = to))
elif ev_type == 'unavailable':
self.jabber.send(Presence(to=fromjid, frm = to, typ = 'unavailable'))
elif ev_type == 'error':
return
else:
self.jabber.send(Presence(to=fromjid, frm = to))
def xmpp_message(self, con, event):
ev_type = event.getType()
fromjid = event.getFrom()
fromstripped = fromjid.getStripped()
to = event.getTo()
## TODO? skip 'error' messages?
## (example: recipient not found, `<message from='…'
## to='…@pymailt.…' type='error' id='1'>…<error code='503'
## type='cancel'>…<service-unavailable
## xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>…`)
if ev_type == 'error':
## Log properly? Send to fallbackjid (if not the error about it)?
try: # hax to plug it in
raise Exception("Error XMPP message", event, str(event))
except Exception as e:
logError()
return
try:
if event.getSubject.strip() == '':
event.setSubject(None)
except AttributeError:
pass
if event.getBody() == None:
return
if to.getNode() == '':
self.jabber.send(Error(event, ERR_ITEM_NOT_FOUND))
return
mto = to.getNode().replace('%', '@')
fromsplit = fromstripped.split('@', 1)
mfrom = None
for mapping in self.mappings:
if mapping[0] == fromsplit[1]:
mfrom = '%s@%s' % (fromsplit[0], mapping[1])
if not mfrom:
self.jabber.send(Error(event, ERR_REGISTRATION_REQUIRED))
return
subject = event.getSubject()
body = event.getBody()
## TODO: Make it possible to ender subject as a part of message
## (e.g. `Sobject: ...` in the first line)
## TODO?: e-mail conversation tracking (reply-to)
charset = 'utf-8'
body = body.encode(charset, 'replace')
msg = MIMEText(body, 'plain', charset)
if subject:
msg['Subject'] = subject
msg['From'] = mfrom
msg['To'] = mto
try:
if config.dumpProtocol:
_log.info('SENDING: %r', msg.as_string())
mailserver = smtplib.SMTP(config.smtpServer)
if config.dumpProtocol:
mailserver.set_debuglevel(1)
mailserver.sendmail(mfrom, mto, msg.as_string())
mailserver.quit()
except:
logError()
self.jabber.send(Error(event, ERR_RECIPIENT_UNAVAILABLE))
def mail_check(self):
if time.time() < self.lastcheck + 5:
return
self.lastcheck = time.time()
mails = os.listdir(self.watchdir)
for mail in mails:
fullname = '%s%s' % (self.watchdir, mail)
fp = open(fullname)
msg = email.message_from_file(fp)
fp.close()
os.remove(fullname)
if config.dumpProtocol:
_log.info('RECEIVING: %r', msg.as_string())
mfrom = email.Utils.parseaddr(msg['From'])[1]
## XXXX: re-check this
mto_base = msg['Envelope-To'] or msg['To']
mto = email.Utils.parseaddr(mto_base)[1]
## XXXX/TODO: use `Message-id` or similar for resource (and
## parse it in incoming messages)? Might have to also send
## status updates for those.
jfrom = '%s@%s' % (mfrom.replace('@', '%'), config.jid)
tosplit = mto.split('@', 1)
jto = None
for mapping in self.mappings:
#break ## XXXXXX: hax: send everything to one place.
if mapping[1] == tosplit[1]:
jto = '%s@%s' % (tosplit [0], mapping[0])
if not jto:
## XXX: actual problem is in, e.g., maillists mail, which is
## sent to the maillist and not to the recipient. This is
## more like a temporary haxfix for that.
jto = self.jto_fallback
if not jto:
continue
(subject, charset) = decode_header(msg['Subject'])[0]
if charset:
subject = unicode(subject, charset, 'replace')
log = _log.debug
log("processing email message %s", repr(msg)[:60])
msg_plain = msg_html = None
submessages = msg.get_payload()
for submessage in submessages:
# msg = msg.get_payload(0)
if not submessage:
continue
ctype = submessage.get_content_type()
# NOTE: 'startswith' might be nore correct, but this should
# be okay too
if 'text/html' in ctype:
log("msg: found text/html")
msg_html = submessage
elif 'text/plain' in ctype:
log("msg: found text/plain")
msg_plain = submessage
else:
log("msg: unprocessed ctype %r" % (ctype,))
if config.preferredFormat == 'plaintext':
log("msg: preferring plaintext")
msg = msg_plain or msg_html or msg # first whatever
else: # html2text or html
log("msg: preferring html")
msg = msg_html or msg_plain or msg
log("msg: resulting content_type is %r" % (msg.get_content_type(),))
charset = msg.get_charsets('utf-8')[0]
body = msg.get_payload(None, True)
body = unicode(body, charset, 'replace')
# check for `msg.get_content_subtype() == 'html'` instead?
if 'text/html' in msg.get_content_type():
if config.preferredFormat != 'html':
log("msg: doing html2text")
html2text = get_html2text(config)
body = html2text(body)
# TODO: else compose an XMPP-HTML message? Will require a
# complicated preprocessor like bs4 though
# TODO?: optional extra headers (e.g. To if To != Envelope-To)
# prepended to the body.
m = Message(to=jto, frm=jfrom, subject=subject, body=body)
self.jabber.send(m)
def xmpp_connect(self):
connected = self.jabber.connect((config.mainServer, config.port))
if config.dumpProtocol:
_log.info("connected: %r", connected)
while not connected:
time.sleep(5)
connected = self.jabber.connect((config.mainServer, config.port))
if config.dumpProtocol:
_log.info("connected: %r", connected)
self.register_handlers()
if config.dumpProtocol:
_log.info("trying auth")
connected = self.jabber.auth(config.saslUsername, config.secret)
if config.dumpProtocol:
_log.info("auth return: %r", connected)
return connected
def xmpp_disconnect(self):
time.sleep(5)
if not self.jabber.reconnectAndReauth():
time.sleep(5)
self.xmpp_connect()
def loadConfig():
configOptions = {}
for configFile in config.configFiles:
if os.path.isfile(configFile):
xmlconfig.reloadConfig(configFile, configOptions)
config.configFile = configFile
return
sys.stderr.write(("Configuration file not found. "
"You need to create a config file and put it "
" in one of these locations:\n ")
+ "\n ".join(config.configFiles))
sys.exit(1)
def logError():
err = '%s - %s\n' % (time.strftime('%a %d %b %Y %H:%M:%S'), version)
if logfile != None:
logfile.write(err)
traceback.print_exc(file=logfile)
logfile.flush()
sys.stderr.write(err)
traceback.print_exc()
sys.exc_clear()
def sigHandler(signum, frame):
transport.offlinemsg = 'Signal handler called with signal %s' % (signum,)
if config.dumpProtocol:
print 'Signal handler called with signal %s' % (signum,)
transport.online = 0
if __name__ == '__main__':
if 'PID' in os.environ:
config.pid = os.environ['PID']
loadConfig()
if config.pid:
pidfile = open(config.pid,'w')
pidfile.write(`os.getpid()`)
pidfile.close()
config_logging(config)
logfile = None
if config.debugFile:
logfile = open(config.debugFile, 'a')
if config.saslUsername:
sasl = 1
else:
config.saslUsername = config.jid
sasl = 0
if config.dumpProtocol:
debug = ['always', 'nodebuilder']
else:
debug = []
connection = xmpp.client.Component(config.jid, config.port, debug=debug,
sasl=sasl, bind=config.useComponentBinding, route=config.useRouteWrap)
transport = Transport(connection)
if not transport.xmpp_connect():
print "Could not connect to server, or password mismatch!"
sys.exit(1)
# Set the signal handlers
signal.signal(signal.SIGINT, sigHandler)
signal.signal(signal.SIGTERM, sigHandler)
transport.lastcheck = time.time() + 10
while transport.online:
try:
connection.Process(1)
transport.mail_check()
except KeyboardInterrupt:
_pendingException = sys.exc_info()
raise _pendingException[0], _pendingException[1], _pendingException[2]
except IOError:
transport.xmpp_disconnect()
except:
logError()
if not connection.isConnected():
transport.xmpp_disconnect()
connection.disconnect()
if config.pid:
os.unlink(config.pid)
if logfile:
logfile.close()
if transport.restart:
args = [sys.executable] + sys.argv
if os.name == 'nt':
args = ["\"%s\"" % (a,) for a in args]
if config.dumpProtocol:
print sys.executable, args
os.execv(sys.executable, args)