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
|
import cherrypy
import threading
from os import getenv
from queue import Queue, Empty
from imap_tools import MailMessage as IMAP_Message
from email.message import Message
from email import message_from_bytes, utils
from smtplib import SMTP_SSL as SMTP
from smtplib import (
SMTPResponseException,
SMTPDataError,
SMTPConnectError,
SMTPServerDisconnected,
SMTPHeloError,
SMTPAuthenticationError
)
from datetime import datetime
class SmtpPlugin():
def __init__(self):
self.smtp_server=getenv("ACIT_SMTP_SERVER")
self.smtp_user=getenv("ACIT_SMTP_USER")
self.smtp_pass=getenv("ACIT_SMTP_PASS")
self.smtp_port=int(getenv("ACIT_SMTP_PORT",0))
self.domain="whyisthisattributeunset.example.com"
self.thread:threading.Thread=None
self.stopping=threading.Event()
self.que=Queue()
def sendmail(self,
from_:str=None,
to:str|list[str]=[],
subject:str=None,
body:str=None,
cc:list[str]=[],
bcc:list[str]=[],
replyto:IMAP_Message=None,
tosend:IMAP_Message|Message=None,
save=False,
):
"""
Required arguments:
if ``tosend`` is unset:
- body, from_
- if ``replyto`` is also unset, also:
- to, subject
if ``tosend`` is set:
- from_
from_ (str): the email address to use for the ``From`` header. if ``tosend`` is set, used for ``Resent-From`` instead, and added to ``Cc``.
to (list|str): the email address(es) to send to. when specifying multiple addrs, use a list.
subject (str): the subject for the email. not needed when ``replyto`` is set, though when both are set, ``subject`` is used.
body (str): the email body
cc (list): email addresses to put in the cc
save (bool): force adding from_ to the cclist, making ImapPlugin store the email in ``Sent`` on arrival
"""
if type(to)==str:
to=[to]
if tosend:
if type(tosend)==IMAP_Message:
tosend=tosend.obj
tosend=message_from_bytes(tosend.as_bytes(), policy=tosend.policy) # copy message object
tosend["Resent-From"]=from_
tosend["Resent-Date"]=utils.format_datetime(datetime.now())
tosend["Resent-To"]=", ".join(to)
if bcc:
tosend["Resent-Bcc"]=", ".join(bcc)
if cc:
tosend["Resent-Cc"]=", ".join(bcc)
if not from_ in tosend["to"]+tosend.get("cc","")+tosend.get("resent-to",""):
if "cc" in tosend:
tosend["cc"]+=", "+from_
else:
tosend["cc"]=from_
tosend["X-Acit-Delete-When-Sender"]=from_
else:
if replyto:
cc.extend(replyto.cc)
cc.extend(replyto.to)
to.append(replyto.headers["reply-to"] if "reply-to" in replyto.headers else replyto.from_)
if not subject:
subject="Re: "+replyto.subject
for addr in to:
if addr in cc:
cc.remove(addr)
for addr in cc:
if addr in bcc:
bcc.remove(addr)
if not (to and body and from_ and subject):
raise ValueError("Missing to, from_, body, or subject")
tosend=Message()
tosend["From"]=from_
tosend["To"]=", ".join(set(to)) # set()s can't have duplicates
tosend["Subject"]=subject
tosend["Cc"]=", ".join(set(cc))
tosend["Bcc"]=", ".join(set(bcc))
if replyto:
if "references" in replyto.headers:
tosend["References"]=replyto.obj["references"]
elif "in-reply-to" in replyto.headers:
tosend["References"]=replyto.obj["in-reply-to"]
if "message-id" in replyto.headers:
tosend["In-Reply-To"]=replyto.obj["message-id"]
if "References" in tosend:
tosend["References"]+=" "+replyto.obj["message-id"]
else:
tosend["References"]=replyto.obj["message-id"]
tosend.set_payload(body)
tosend["Sender"]=from_
tosend["X-Acit-Is-Outgoing"]=from_
if not "Message-ID" in tosend:
tosend["Message-ID"]="<"+datetime.now().strftime("%d%m%Y%H%M%S%j")+"@"+self.domain+">"
if save and not from_ in tosend["Cc"]:
tosend["Cc"]+=" "+from_
self.que.put(tosend)
return tosend["Message-ID"]
def start(self):
self.thread=threading.Thread(target=self.runner)
self.thread.start()
def runner(self):
threading.current_thread().name="SendMailRunner"
smtp=None
while True:
try:
item=self.que.get(timeout=1)
if not item:
continue
except Empty:
if smtp:
smtp.quit()
smtp=None
if self.stopping.is_set():
break
continue
try:
if not smtp:
self.mlog("Connecting and logging in to smtp server")
smtp=SMTP(host=self.smtp_server,port=self.smtp_port)
smtp.ehlo()
smtp.login(user=self.smtp_user,password=self.smtp_pass)
self.mlog("Sending message %s '%s'"%(item.get("message-id"),item.get("subject")))
smtp.send_message(item)
except SMTPAuthenticationError:
self.mlog("!!! AUTH ERROR !!! please check your config. Discarding email.")
except (SMTPHeloError,SMTPResponseException,SMTPDataError,SMTPServerDisconnected,SMTPConnectError) as e:
self.mlog("Error occured: %s"%repr(e))
self.que.put(item)
self.mlog("Sending rescheduled.")
except Exception:
if smtp:
smtp.quit()
smtp=None
import traceback
self.mlog("An error occured in the SMTP runner:\n",traceback.format_exc())
self.mlog("As a result, the following email was discarded: %s '%s'"%(item.get("message-id"),item.get("subject")))
finally:
self.que.task_done()
def stop(self,block=True):
self.mlog("Signaling stop to sendmail")
self.stopping.set()
if block:
self.thread.join()
def mlog(self,*msg,**kwargs):
import traceback
function=traceback.extract_stack(limit=2)[0].name
cherrypy.log(context="%s>>SENDMAIL:%s"%(threading.current_thread().name, function), msg=" ".join([str(i) for i in msg]),**kwargs)
|