aboutsummaryrefslogtreecommitdiffstats
path: root/src/acit/smtpplugin.py
blob: 5f3ebfb0e85ab3dd77003ad4ee1a1c59318ca586 (about) (plain)
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)