aprsd 1.0.0__py3-none-any.whl → 3.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aprsd/__init__.py +6 -4
- aprsd/cli_helper.py +151 -0
- aprsd/client/__init__.py +13 -0
- aprsd/client/aprsis.py +132 -0
- aprsd/client/base.py +105 -0
- aprsd/client/drivers/__init__.py +0 -0
- aprsd/client/drivers/aprsis.py +228 -0
- aprsd/client/drivers/fake.py +73 -0
- aprsd/client/drivers/kiss.py +119 -0
- aprsd/client/factory.py +88 -0
- aprsd/client/fake.py +48 -0
- aprsd/client/kiss.py +103 -0
- aprsd/client/stats.py +38 -0
- aprsd/cmds/__init__.py +0 -0
- aprsd/cmds/completion.py +22 -0
- aprsd/cmds/dev.py +162 -0
- aprsd/cmds/fetch_stats.py +156 -0
- aprsd/cmds/healthcheck.py +86 -0
- aprsd/cmds/list_plugins.py +319 -0
- aprsd/cmds/listen.py +231 -0
- aprsd/cmds/send_message.py +171 -0
- aprsd/cmds/server.py +137 -0
- aprsd/cmds/webchat.py +674 -0
- aprsd/conf/__init__.py +56 -0
- aprsd/conf/client.py +131 -0
- aprsd/conf/common.py +301 -0
- aprsd/conf/log.py +65 -0
- aprsd/conf/opts.py +80 -0
- aprsd/conf/plugin_common.py +182 -0
- aprsd/conf/plugin_email.py +105 -0
- aprsd/exception.py +13 -0
- aprsd/log/__init__.py +0 -0
- aprsd/log/log.py +138 -0
- aprsd/main.py +104 -867
- aprsd/packets/__init__.py +20 -0
- aprsd/packets/collector.py +79 -0
- aprsd/packets/core.py +823 -0
- aprsd/packets/log.py +161 -0
- aprsd/packets/packet_list.py +110 -0
- aprsd/packets/seen_list.py +49 -0
- aprsd/packets/tracker.py +103 -0
- aprsd/packets/watch_list.py +119 -0
- aprsd/plugin.py +474 -284
- aprsd/plugin_utils.py +86 -0
- aprsd/plugins/__init__.py +0 -0
- aprsd/plugins/email.py +709 -0
- aprsd/plugins/fortune.py +61 -0
- aprsd/plugins/location.py +179 -0
- aprsd/plugins/notify.py +61 -0
- aprsd/plugins/ping.py +31 -0
- aprsd/plugins/time.py +115 -0
- aprsd/plugins/version.py +31 -0
- aprsd/plugins/weather.py +405 -0
- aprsd/stats/__init__.py +20 -0
- aprsd/stats/app.py +49 -0
- aprsd/stats/collector.py +37 -0
- aprsd/threads/__init__.py +11 -0
- aprsd/threads/aprsd.py +119 -0
- aprsd/threads/keep_alive.py +131 -0
- aprsd/threads/log_monitor.py +121 -0
- aprsd/threads/registry.py +56 -0
- aprsd/threads/rx.py +354 -0
- aprsd/threads/stats.py +44 -0
- aprsd/threads/tx.py +255 -0
- aprsd/utils/__init__.py +218 -0
- aprsd/utils/counter.py +51 -0
- aprsd/utils/json.py +80 -0
- aprsd/utils/objectstore.py +123 -0
- aprsd/utils/ring_buffer.py +40 -0
- aprsd/utils/trace.py +180 -0
- aprsd/web/__init__.py +0 -0
- aprsd/web/admin/__init__.py +0 -0
- aprsd/web/admin/static/css/index.css +84 -0
- aprsd/web/admin/static/css/prism.css +4 -0
- aprsd/web/admin/static/css/tabs.css +35 -0
- aprsd/web/admin/static/images/Untitled.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-16-0.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-16-1.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-0.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-1.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-2.png +0 -0
- aprsd/web/admin/static/js/charts.js +235 -0
- aprsd/web/admin/static/js/echarts.js +465 -0
- aprsd/web/admin/static/js/logs.js +26 -0
- aprsd/web/admin/static/js/main.js +231 -0
- aprsd/web/admin/static/js/prism.js +12 -0
- aprsd/web/admin/static/js/send-message.js +114 -0
- aprsd/web/admin/static/js/tabs.js +28 -0
- aprsd/web/admin/templates/index.html +196 -0
- aprsd/web/chat/static/css/chat.css +115 -0
- aprsd/web/chat/static/css/index.css +66 -0
- aprsd/web/chat/static/css/style.css.map +1 -0
- aprsd/web/chat/static/css/tabs.css +41 -0
- aprsd/web/chat/static/css/upstream/bootstrap.min.css +6 -0
- aprsd/web/chat/static/css/upstream/font.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/google-fonts.css +23 -0
- aprsd/web/chat/static/css/upstream/jquery-ui.css +1311 -0
- aprsd/web/chat/static/css/upstream/jquery.toast.css +28 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff2 +0 -0
- aprsd/web/chat/static/images/Untitled.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-16-0.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-16-1.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-0.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-1.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-2.png +0 -0
- aprsd/web/chat/static/images/globe.svg +3 -0
- aprsd/web/chat/static/js/gps.js +84 -0
- aprsd/web/chat/static/js/main.js +45 -0
- aprsd/web/chat/static/js/send-message.js +585 -0
- aprsd/web/chat/static/js/tabs.js +28 -0
- aprsd/web/chat/static/js/upstream/bootstrap.bundle.min.js +7 -0
- aprsd/web/chat/static/js/upstream/jquery-3.7.1.min.js +2 -0
- aprsd/web/chat/static/js/upstream/jquery-ui.min.js +13 -0
- aprsd/web/chat/static/js/upstream/jquery.toast.js +374 -0
- aprsd/web/chat/static/js/upstream/semantic.min.js +11 -0
- aprsd/web/chat/static/js/upstream/socket.io.min.js +7 -0
- aprsd/web/chat/templates/index.html +139 -0
- aprsd/wsgi.py +315 -0
- aprsd-3.4.2.dist-info/AUTHORS +13 -0
- aprsd-3.4.2.dist-info/LICENSE +175 -0
- aprsd-3.4.2.dist-info/METADATA +793 -0
- aprsd-3.4.2.dist-info/RECORD +133 -0
- {aprsd-1.0.0.dist-info → aprsd-3.4.2.dist-info}/WHEEL +1 -1
- aprsd-3.4.2.dist-info/entry_points.txt +8 -0
- aprsd/fake_aprs.py +0 -83
- aprsd/utils.py +0 -166
- aprsd-1.0.0.dist-info/AUTHORS +0 -6
- aprsd-1.0.0.dist-info/METADATA +0 -181
- aprsd-1.0.0.dist-info/RECORD +0 -13
- aprsd-1.0.0.dist-info/entry_points.txt +0 -4
- aprsd-1.0.0.dist-info/pbr.json +0 -1
- /aprsd/{fuzzyclock.py → utils/fuzzyclock.py} +0 -0
- {aprsd-1.0.0.dist-info → aprsd-3.4.2.dist-info}/top_level.txt +0 -0
aprsd/plugins/email.py
ADDED
@@ -0,0 +1,709 @@
|
|
1
|
+
import datetime
|
2
|
+
import email
|
3
|
+
from email.mime.text import MIMEText
|
4
|
+
import imaplib
|
5
|
+
import logging
|
6
|
+
import re
|
7
|
+
import smtplib
|
8
|
+
import threading
|
9
|
+
import time
|
10
|
+
|
11
|
+
import imapclient
|
12
|
+
from oslo_config import cfg
|
13
|
+
|
14
|
+
from aprsd import packets, plugin, threads, utils
|
15
|
+
from aprsd.threads import tx
|
16
|
+
from aprsd.utils import trace
|
17
|
+
|
18
|
+
|
19
|
+
CONF = cfg.CONF
|
20
|
+
LOG = logging.getLogger("APRSD")
|
21
|
+
shortcuts_dict = None
|
22
|
+
|
23
|
+
|
24
|
+
class EmailInfo:
|
25
|
+
"""A singleton thread safe mechanism for the global check_email_delay.
|
26
|
+
|
27
|
+
This has to be done because we have 2 separate threads that access
|
28
|
+
the delay value.
|
29
|
+
1) when EmailPlugin runs from a user message and
|
30
|
+
2) when the background EmailThread runs to check email.
|
31
|
+
|
32
|
+
Access the check email delay with
|
33
|
+
EmailInfo().delay
|
34
|
+
|
35
|
+
Set it with
|
36
|
+
EmailInfo().delay = 100
|
37
|
+
or
|
38
|
+
EmailInfo().delay += 10
|
39
|
+
|
40
|
+
"""
|
41
|
+
|
42
|
+
_instance = None
|
43
|
+
|
44
|
+
def __new__(cls, *args, **kwargs):
|
45
|
+
"""This magic turns this into a singleton."""
|
46
|
+
if cls._instance is None:
|
47
|
+
cls._instance = super().__new__(cls)
|
48
|
+
cls._instance.lock = threading.Lock()
|
49
|
+
cls._instance._delay = 60
|
50
|
+
return cls._instance
|
51
|
+
|
52
|
+
@property
|
53
|
+
def delay(self):
|
54
|
+
with self.lock:
|
55
|
+
return self._delay
|
56
|
+
|
57
|
+
@delay.setter
|
58
|
+
def delay(self, val):
|
59
|
+
with self.lock:
|
60
|
+
self._delay = val
|
61
|
+
|
62
|
+
|
63
|
+
@utils.singleton
|
64
|
+
class EmailStats:
|
65
|
+
"""Singleton object to store stats related to email."""
|
66
|
+
_instance = None
|
67
|
+
tx = 0
|
68
|
+
rx = 0
|
69
|
+
email_thread_last_time = None
|
70
|
+
|
71
|
+
def stats(self, serializable=False):
|
72
|
+
if CONF.email_plugin.enabled:
|
73
|
+
last_check_time = self.email_thread_last_time
|
74
|
+
if serializable and last_check_time:
|
75
|
+
last_check_time = last_check_time.isoformat()
|
76
|
+
stats = {
|
77
|
+
"tx": self.tx,
|
78
|
+
"rx": self.rx,
|
79
|
+
"last_check_time": last_check_time,
|
80
|
+
}
|
81
|
+
else:
|
82
|
+
stats = {}
|
83
|
+
return stats
|
84
|
+
|
85
|
+
def tx_inc(self):
|
86
|
+
self.tx += 1
|
87
|
+
|
88
|
+
def rx_inc(self):
|
89
|
+
self.rx += 1
|
90
|
+
|
91
|
+
def email_thread_update(self):
|
92
|
+
self.email_thread_last_time = datetime.datetime.now()
|
93
|
+
|
94
|
+
|
95
|
+
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
96
|
+
"""Email Plugin."""
|
97
|
+
|
98
|
+
command_regex = "^-.*"
|
99
|
+
command_name = "email"
|
100
|
+
short_description = "Send and Receive email"
|
101
|
+
|
102
|
+
# message_number:time combos so we don't resend the same email in
|
103
|
+
# five mins {int:int}
|
104
|
+
email_sent_dict = {}
|
105
|
+
enabled = False
|
106
|
+
|
107
|
+
def setup(self):
|
108
|
+
"""Ensure that email is enabled and start the thread."""
|
109
|
+
if CONF.email_plugin.enabled:
|
110
|
+
self.enabled = True
|
111
|
+
|
112
|
+
if not CONF.email_plugin.callsign:
|
113
|
+
self.enabled = False
|
114
|
+
LOG.error("email_plugin.callsign is not set.")
|
115
|
+
return
|
116
|
+
|
117
|
+
if not CONF.email_plugin.imap_login:
|
118
|
+
LOG.error("email_plugin.imap_login not set. Disabling Plugin")
|
119
|
+
self.enabled = False
|
120
|
+
return
|
121
|
+
|
122
|
+
if not CONF.email_plugin.smtp_login:
|
123
|
+
LOG.error("email_plugin.smtp_login not set. Disabling Plugin")
|
124
|
+
self.enabled = False
|
125
|
+
return
|
126
|
+
|
127
|
+
shortcuts = _build_shortcuts_dict()
|
128
|
+
LOG.info(f"Email shortcuts {shortcuts}")
|
129
|
+
else:
|
130
|
+
LOG.info("Email services not enabled.")
|
131
|
+
self.enabled = False
|
132
|
+
|
133
|
+
def create_threads(self):
|
134
|
+
if self.enabled:
|
135
|
+
return APRSDEmailThread()
|
136
|
+
|
137
|
+
@trace.trace
|
138
|
+
def process(self, packet: packets.MessagePacket):
|
139
|
+
LOG.info("Email COMMAND")
|
140
|
+
if not self.enabled:
|
141
|
+
# Email has not been enabled
|
142
|
+
# so the plugin will just NOOP
|
143
|
+
return packets.NULL_MESSAGE
|
144
|
+
|
145
|
+
fromcall = packet.from_call
|
146
|
+
message = packet.message_text
|
147
|
+
ack = packet.get("msgNo", "0")
|
148
|
+
|
149
|
+
reply = None
|
150
|
+
if not CONF.email_plugin.enabled:
|
151
|
+
LOG.debug("Email is not enabled in config file ignoring.")
|
152
|
+
return "Email not enabled."
|
153
|
+
|
154
|
+
searchstring = "^" + CONF.email_plugin.callsign + ".*"
|
155
|
+
# only I can do email
|
156
|
+
if re.search(searchstring, fromcall):
|
157
|
+
# digits only, first one is number of emails to resend
|
158
|
+
r = re.search("^-([0-9])[0-9]*$", message)
|
159
|
+
if r is not None:
|
160
|
+
LOG.debug("RESEND EMAIL")
|
161
|
+
resend_email(r.group(1), fromcall)
|
162
|
+
reply = packets.NULL_MESSAGE
|
163
|
+
# -user@address.com body of email
|
164
|
+
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
|
165
|
+
# (same search again)
|
166
|
+
a = re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message)
|
167
|
+
if a is not None:
|
168
|
+
to_addr = a.group(1)
|
169
|
+
content = a.group(2)
|
170
|
+
|
171
|
+
email_address = get_email_from_shortcut(to_addr)
|
172
|
+
if not email_address:
|
173
|
+
reply = "Bad email address"
|
174
|
+
return reply
|
175
|
+
|
176
|
+
# send recipient link to aprs.fi map
|
177
|
+
if content == "mapme":
|
178
|
+
content = (
|
179
|
+
"Click for my location: http://aprs.fi/{}" ""
|
180
|
+
).format(
|
181
|
+
CONF.email_plugin.callsign,
|
182
|
+
)
|
183
|
+
too_soon = 0
|
184
|
+
now = time.time()
|
185
|
+
# see if we sent this msg number recently
|
186
|
+
if ack in self.email_sent_dict:
|
187
|
+
# BUG(hemna) - when we get a 2 different email command
|
188
|
+
# with the same ack #, we don't send it.
|
189
|
+
timedelta = now - self.email_sent_dict[ack]
|
190
|
+
if timedelta < 300: # five minutes
|
191
|
+
too_soon = 1
|
192
|
+
if not too_soon or ack == 0:
|
193
|
+
LOG.info(f"Send email '{content}'")
|
194
|
+
send_result = send_email(to_addr, content)
|
195
|
+
reply = packets.NULL_MESSAGE
|
196
|
+
if send_result != 0:
|
197
|
+
reply = f"-{to_addr} failed"
|
198
|
+
else:
|
199
|
+
# clear email sent dictionary if somehow goes
|
200
|
+
# over 100
|
201
|
+
if len(self.email_sent_dict) > 98:
|
202
|
+
LOG.debug(
|
203
|
+
"DEBUG: email_sent_dict is big ("
|
204
|
+
+ str(len(self.email_sent_dict))
|
205
|
+
+ ") clearing out.",
|
206
|
+
)
|
207
|
+
self.email_sent_dict.clear()
|
208
|
+
self.email_sent_dict[ack] = now
|
209
|
+
else:
|
210
|
+
reply = packets.NULL_MESSAGE
|
211
|
+
LOG.info(
|
212
|
+
"Email for message number "
|
213
|
+
+ ack
|
214
|
+
+ " recently sent, not sending again.",
|
215
|
+
)
|
216
|
+
else:
|
217
|
+
reply = "Bad email address"
|
218
|
+
|
219
|
+
return reply
|
220
|
+
|
221
|
+
|
222
|
+
def _imap_connect():
|
223
|
+
imap_port = CONF.email_plugin.imap_port
|
224
|
+
use_ssl = CONF.email_plugin.imap_use_ssl
|
225
|
+
|
226
|
+
try:
|
227
|
+
server = imapclient.IMAPClient(
|
228
|
+
CONF.email_plugin.imap_host,
|
229
|
+
port=imap_port,
|
230
|
+
use_uid=True,
|
231
|
+
ssl=use_ssl,
|
232
|
+
timeout=30,
|
233
|
+
)
|
234
|
+
except Exception:
|
235
|
+
LOG.exception("Failed to connect IMAP server")
|
236
|
+
return
|
237
|
+
|
238
|
+
try:
|
239
|
+
server.login(
|
240
|
+
CONF.email_plugin.imap_login,
|
241
|
+
CONF.email_plugin.imap_password,
|
242
|
+
)
|
243
|
+
except (imaplib.IMAP4.error, Exception) as e:
|
244
|
+
msg = getattr(e, "message", repr(e))
|
245
|
+
LOG.error(f"Failed to login {msg}")
|
246
|
+
return
|
247
|
+
|
248
|
+
server.select_folder("INBOX")
|
249
|
+
|
250
|
+
server.fetch = trace.trace(server.fetch)
|
251
|
+
server.search = trace.trace(server.search)
|
252
|
+
server.remove_flags = trace.trace(server.remove_flags)
|
253
|
+
server.add_flags = trace.trace(server.add_flags)
|
254
|
+
return server
|
255
|
+
|
256
|
+
|
257
|
+
def _smtp_connect():
|
258
|
+
host = CONF.email_plugin.smtp_host
|
259
|
+
smtp_port = CONF.email_plugin.smtp_port
|
260
|
+
use_ssl = CONF.email_plugin.smtp_use_ssl
|
261
|
+
msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
|
262
|
+
LOG.debug(
|
263
|
+
"Connect to SMTP host {} with user '{}'".format(
|
264
|
+
msg,
|
265
|
+
CONF.email_plugin.smtp_login,
|
266
|
+
),
|
267
|
+
)
|
268
|
+
|
269
|
+
try:
|
270
|
+
if use_ssl:
|
271
|
+
server = smtplib.SMTP_SSL(
|
272
|
+
host=host,
|
273
|
+
port=smtp_port,
|
274
|
+
timeout=30,
|
275
|
+
)
|
276
|
+
else:
|
277
|
+
server = smtplib.SMTP(
|
278
|
+
host=host,
|
279
|
+
port=smtp_port,
|
280
|
+
timeout=30,
|
281
|
+
)
|
282
|
+
except Exception:
|
283
|
+
LOG.error("Couldn't connect to SMTP Server")
|
284
|
+
return
|
285
|
+
|
286
|
+
LOG.debug(f"Connected to smtp host {msg}")
|
287
|
+
|
288
|
+
debug = CONF.email_plugin.debug
|
289
|
+
if debug:
|
290
|
+
server.set_debuglevel(5)
|
291
|
+
server.sendmail = trace.trace(server.sendmail)
|
292
|
+
|
293
|
+
try:
|
294
|
+
server.login(
|
295
|
+
CONF.email_plugin.smtp_login,
|
296
|
+
CONF.email_plugin.smtp_password,
|
297
|
+
)
|
298
|
+
except Exception:
|
299
|
+
LOG.error("Couldn't connect to SMTP Server")
|
300
|
+
return
|
301
|
+
|
302
|
+
LOG.debug(f"Logged into SMTP server {msg}")
|
303
|
+
return server
|
304
|
+
|
305
|
+
|
306
|
+
def _build_shortcuts_dict():
|
307
|
+
global shortcuts_dict
|
308
|
+
if not shortcuts_dict:
|
309
|
+
if CONF.email_plugin.email_shortcuts:
|
310
|
+
shortcuts_dict = {}
|
311
|
+
tmp = CONF.email_plugin.email_shortcuts
|
312
|
+
for combo in tmp:
|
313
|
+
entry = combo.split("=")
|
314
|
+
shortcuts_dict[entry[0]] = entry[1]
|
315
|
+
else:
|
316
|
+
shortcuts_dict = {}
|
317
|
+
|
318
|
+
return shortcuts_dict
|
319
|
+
|
320
|
+
|
321
|
+
def get_email_from_shortcut(addr):
|
322
|
+
if CONF.email_plugin.email_shortcuts:
|
323
|
+
shortcuts = _build_shortcuts_dict()
|
324
|
+
LOG.info(f"Shortcut lookup {addr} returns {shortcuts.get(addr, addr)}")
|
325
|
+
return shortcuts.get(addr, addr)
|
326
|
+
else:
|
327
|
+
return addr
|
328
|
+
|
329
|
+
|
330
|
+
def validate_email_config(disable_validation=False):
|
331
|
+
"""function to simply ensure we can connect to email services.
|
332
|
+
|
333
|
+
This helps with failing early during startup.
|
334
|
+
"""
|
335
|
+
LOG.info("Checking IMAP configuration")
|
336
|
+
imap_server = _imap_connect()
|
337
|
+
LOG.info("Checking SMTP configuration")
|
338
|
+
smtp_server = _smtp_connect()
|
339
|
+
|
340
|
+
if imap_server and smtp_server:
|
341
|
+
return True
|
342
|
+
else:
|
343
|
+
return False
|
344
|
+
|
345
|
+
|
346
|
+
@trace.trace
|
347
|
+
def parse_email(msgid, data, server):
|
348
|
+
envelope = data[b"ENVELOPE"]
|
349
|
+
# email address match
|
350
|
+
# use raw string to avoid invalid escape secquence errors r"string here"
|
351
|
+
f = re.search(r"([\.\w_-]+@[\.\w_-]+)", str(envelope.from_[0]))
|
352
|
+
if f is not None:
|
353
|
+
from_addr = f.group(1)
|
354
|
+
else:
|
355
|
+
from_addr = "noaddr"
|
356
|
+
LOG.debug(f"Got a message from '{from_addr}'")
|
357
|
+
try:
|
358
|
+
m = server.fetch([msgid], ["RFC822"])
|
359
|
+
except Exception:
|
360
|
+
LOG.exception("Couldn't fetch email from server in parse_email")
|
361
|
+
return
|
362
|
+
|
363
|
+
msg = email.message_from_string(m[msgid][b"RFC822"].decode(errors="ignore"))
|
364
|
+
if msg.is_multipart():
|
365
|
+
text = ""
|
366
|
+
html = None
|
367
|
+
# default in case body somehow isn't set below - happened once
|
368
|
+
body = b"* unreadable msg received"
|
369
|
+
# this uses the last text or html part in the email,
|
370
|
+
# phone companies often put content in an attachment
|
371
|
+
for part in msg.get_payload():
|
372
|
+
if part.get_content_charset() is None:
|
373
|
+
# or BREAK when we hit a text or html?
|
374
|
+
# We cannot know the character set,
|
375
|
+
# so return decoded "something"
|
376
|
+
LOG.debug("Email got unknown content type")
|
377
|
+
text = part.get_payload(decode=True)
|
378
|
+
continue
|
379
|
+
|
380
|
+
charset = part.get_content_charset()
|
381
|
+
|
382
|
+
if part.get_content_type() == "text/plain":
|
383
|
+
LOG.debug("Email got text/plain")
|
384
|
+
text = str(
|
385
|
+
part.get_payload(decode=True),
|
386
|
+
str(charset),
|
387
|
+
"ignore",
|
388
|
+
).encode("utf8", "replace")
|
389
|
+
|
390
|
+
if part.get_content_type() == "text/html":
|
391
|
+
LOG.debug("Email got text/html")
|
392
|
+
html = str(
|
393
|
+
part.get_payload(decode=True),
|
394
|
+
str(charset),
|
395
|
+
"ignore",
|
396
|
+
).encode("utf8", "replace")
|
397
|
+
|
398
|
+
if text is not None:
|
399
|
+
# strip removes white space fore and aft of string
|
400
|
+
body = text.strip()
|
401
|
+
else:
|
402
|
+
body = html.strip()
|
403
|
+
else: # message is not multipart
|
404
|
+
# email.uscc.net sends no charset, blows up unicode function below
|
405
|
+
LOG.debug("Email is not multipart")
|
406
|
+
if msg.get_content_charset() is None:
|
407
|
+
text = str(msg.get_payload(decode=True), "US-ASCII", "ignore").encode(
|
408
|
+
"utf8",
|
409
|
+
"replace",
|
410
|
+
)
|
411
|
+
else:
|
412
|
+
text = str(
|
413
|
+
msg.get_payload(decode=True),
|
414
|
+
msg.get_content_charset(),
|
415
|
+
"ignore",
|
416
|
+
).encode("utf8", "replace")
|
417
|
+
body = text.strip()
|
418
|
+
|
419
|
+
# FIXED: UnicodeDecodeError: 'ascii' codec can't decode byte 0xf0
|
420
|
+
# in position 6: ordinal not in range(128)
|
421
|
+
# decode with errors='ignore'. be sure to encode it before we return
|
422
|
+
# it below, also with errors='ignore'
|
423
|
+
try:
|
424
|
+
body = body.decode(errors="ignore")
|
425
|
+
except Exception:
|
426
|
+
LOG.exception("Unicode decode failure")
|
427
|
+
LOG.error(f"Unidoce decode failed: {str(body)}")
|
428
|
+
body = "Unreadable unicode msg"
|
429
|
+
# strip all html tags
|
430
|
+
body = re.sub("<[^<]+?>", "", body)
|
431
|
+
# strip CR/LF, make it one line, .rstrip fails at this
|
432
|
+
body = body.replace("\n", " ").replace("\r", " ")
|
433
|
+
# ascii might be out of range, so encode it, removing any error characters
|
434
|
+
body = body.encode(errors="ignore")
|
435
|
+
return body, from_addr
|
436
|
+
|
437
|
+
|
438
|
+
# end parse_email
|
439
|
+
|
440
|
+
|
441
|
+
@trace.trace
|
442
|
+
def send_email(to_addr, content):
|
443
|
+
shortcuts = _build_shortcuts_dict()
|
444
|
+
email_address = get_email_from_shortcut(to_addr)
|
445
|
+
LOG.info("Sending Email_________________")
|
446
|
+
|
447
|
+
if to_addr in shortcuts:
|
448
|
+
LOG.info(f"To : {to_addr}")
|
449
|
+
to_addr = email_address
|
450
|
+
LOG.info(f" ({to_addr})")
|
451
|
+
subject = CONF.email_plugin.callsign
|
452
|
+
# content = content + "\n\n(NOTE: reply with one line)"
|
453
|
+
LOG.info(f"Subject : {subject}")
|
454
|
+
LOG.info(f"Body : {content}")
|
455
|
+
|
456
|
+
# check email more often since there's activity right now
|
457
|
+
EmailInfo().delay = 60
|
458
|
+
|
459
|
+
msg = MIMEText(content)
|
460
|
+
msg["Subject"] = subject
|
461
|
+
msg["From"] = CONF.email_plugin.smtp_login
|
462
|
+
msg["To"] = to_addr
|
463
|
+
server = _smtp_connect()
|
464
|
+
if server:
|
465
|
+
try:
|
466
|
+
server.sendmail(
|
467
|
+
CONF.email_plugin.smtp_login,
|
468
|
+
[to_addr],
|
469
|
+
msg.as_string(),
|
470
|
+
)
|
471
|
+
EmailStats().tx_inc()
|
472
|
+
except Exception:
|
473
|
+
LOG.exception("Sendmail Error!!!!")
|
474
|
+
server.quit()
|
475
|
+
return -1
|
476
|
+
server.quit()
|
477
|
+
return 0
|
478
|
+
|
479
|
+
|
480
|
+
@trace.trace
|
481
|
+
def resend_email(count, fromcall):
|
482
|
+
date = datetime.datetime.now()
|
483
|
+
month = date.strftime("%B")[:3] # Nov, Mar, Apr
|
484
|
+
day = date.day
|
485
|
+
year = date.year
|
486
|
+
today = f"{day}-{month}-{year}"
|
487
|
+
|
488
|
+
shortcuts = _build_shortcuts_dict()
|
489
|
+
# swap key/value
|
490
|
+
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
491
|
+
|
492
|
+
try:
|
493
|
+
server = _imap_connect()
|
494
|
+
except Exception:
|
495
|
+
LOG.exception("Failed to Connect to IMAP. Cannot resend email ")
|
496
|
+
return
|
497
|
+
|
498
|
+
try:
|
499
|
+
messages = server.search(["SINCE", today])
|
500
|
+
except Exception:
|
501
|
+
LOG.exception("Couldn't search for emails in resend_email ")
|
502
|
+
return
|
503
|
+
|
504
|
+
# LOG.debug("%d messages received today" % len(messages))
|
505
|
+
|
506
|
+
msgexists = False
|
507
|
+
|
508
|
+
messages.sort(reverse=True)
|
509
|
+
del messages[int(count) :] # only the latest "count" messages
|
510
|
+
for message in messages:
|
511
|
+
try:
|
512
|
+
parts = server.fetch(message, ["ENVELOPE"]).items()
|
513
|
+
except Exception:
|
514
|
+
LOG.exception("Couldn't fetch email parts in resend_email")
|
515
|
+
continue
|
516
|
+
|
517
|
+
for msgid, data in list(parts):
|
518
|
+
# one at a time, otherwise order is random
|
519
|
+
(body, from_addr) = parse_email(msgid, data, server)
|
520
|
+
# unset seen flag, will stay bold in email client
|
521
|
+
try:
|
522
|
+
server.remove_flags(msgid, [imapclient.SEEN])
|
523
|
+
except Exception:
|
524
|
+
LOG.exception("Failed to remove SEEN flag in resend_email")
|
525
|
+
|
526
|
+
if from_addr in shortcuts_inverted:
|
527
|
+
# reverse lookup of a shortcut
|
528
|
+
from_addr = shortcuts_inverted[from_addr]
|
529
|
+
# asterisk indicates a resend
|
530
|
+
reply = "-" + from_addr + " * " + body.decode(errors="ignore")
|
531
|
+
tx.send(
|
532
|
+
packets.MessagePacket(
|
533
|
+
from_call=CONF.callsign,
|
534
|
+
to_call=fromcall,
|
535
|
+
message_text=reply,
|
536
|
+
),
|
537
|
+
)
|
538
|
+
msgexists = True
|
539
|
+
|
540
|
+
if msgexists is not True:
|
541
|
+
stm = time.localtime()
|
542
|
+
h = stm.tm_hour
|
543
|
+
m = stm.tm_min
|
544
|
+
s = stm.tm_sec
|
545
|
+
# append time as a kind of serial number to prevent FT1XDR from
|
546
|
+
# thinking this is a duplicate message.
|
547
|
+
# The FT1XDR pretty much ignores the aprs message number in this
|
548
|
+
# regard. The FTM400 gets it right.
|
549
|
+
reply = "No new msg {}:{}:{}".format(
|
550
|
+
str(h).zfill(2),
|
551
|
+
str(m).zfill(2),
|
552
|
+
str(s).zfill(2),
|
553
|
+
)
|
554
|
+
tx.send(
|
555
|
+
packets.MessagePacket(
|
556
|
+
from_call=CONF.callsign,
|
557
|
+
to_call=fromcall,
|
558
|
+
message_text=reply,
|
559
|
+
),
|
560
|
+
)
|
561
|
+
|
562
|
+
# check email more often since we're resending one now
|
563
|
+
EmailInfo().delay = 60
|
564
|
+
|
565
|
+
server.logout()
|
566
|
+
# end resend_email()
|
567
|
+
|
568
|
+
|
569
|
+
class APRSDEmailThread(threads.APRSDThread):
|
570
|
+
def __init__(self):
|
571
|
+
super().__init__("EmailThread")
|
572
|
+
self.past = datetime.datetime.now()
|
573
|
+
|
574
|
+
def loop(self):
|
575
|
+
time.sleep(5)
|
576
|
+
EmailStats().email_thread_update()
|
577
|
+
# always sleep for 5 seconds and see if we need to check email
|
578
|
+
# This allows CTRL-C to stop the execution of this loop sooner
|
579
|
+
# than check_email_delay time
|
580
|
+
now = datetime.datetime.now()
|
581
|
+
if now - self.past > datetime.timedelta(seconds=EmailInfo().delay):
|
582
|
+
# It's time to check email
|
583
|
+
|
584
|
+
# slowly increase delay every iteration, max out at 300 seconds
|
585
|
+
# any send/receive/resend activity will reset this to 60 seconds
|
586
|
+
if EmailInfo().delay < 300:
|
587
|
+
EmailInfo().delay += 10
|
588
|
+
LOG.debug(
|
589
|
+
f"check_email_delay is {EmailInfo().delay} seconds ",
|
590
|
+
)
|
591
|
+
|
592
|
+
shortcuts = _build_shortcuts_dict()
|
593
|
+
# swap key/value
|
594
|
+
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
595
|
+
|
596
|
+
date = datetime.datetime.now()
|
597
|
+
month = date.strftime("%B")[:3] # Nov, Mar, Apr
|
598
|
+
day = date.day
|
599
|
+
year = date.year
|
600
|
+
today = f"{day}-{month}-{year}"
|
601
|
+
|
602
|
+
try:
|
603
|
+
server = _imap_connect()
|
604
|
+
except Exception:
|
605
|
+
LOG.exception("IMAP Failed to connect")
|
606
|
+
return True
|
607
|
+
|
608
|
+
try:
|
609
|
+
messages = server.search(["SINCE", today])
|
610
|
+
except Exception:
|
611
|
+
LOG.exception("IMAP failed to search for messages since today.")
|
612
|
+
return True
|
613
|
+
LOG.debug(f"{len(messages)} messages received today")
|
614
|
+
|
615
|
+
try:
|
616
|
+
_msgs = server.fetch(messages, ["ENVELOPE"])
|
617
|
+
except Exception:
|
618
|
+
LOG.exception("IMAP failed to fetch/flag messages: ")
|
619
|
+
return True
|
620
|
+
|
621
|
+
for msgid, data in _msgs.items():
|
622
|
+
envelope = data[b"ENVELOPE"]
|
623
|
+
LOG.debug(
|
624
|
+
'ID:%d "%s" (%s)'
|
625
|
+
% (msgid, envelope.subject.decode(), envelope.date),
|
626
|
+
)
|
627
|
+
f = re.search(
|
628
|
+
r"'([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)",
|
629
|
+
str(envelope.from_[0]),
|
630
|
+
)
|
631
|
+
if f is not None:
|
632
|
+
from_addr = f.group(1)
|
633
|
+
else:
|
634
|
+
from_addr = "noaddr"
|
635
|
+
|
636
|
+
# LOG.debug("Message flags/tags: " +
|
637
|
+
# str(server.get_flags(msgid)[msgid]))
|
638
|
+
# if "APRS" not in server.get_flags(msgid)[msgid]:
|
639
|
+
# in python3, imap tags are unicode. in py2 they're strings.
|
640
|
+
# so .decode them to handle both
|
641
|
+
try:
|
642
|
+
taglist = [
|
643
|
+
x.decode(errors="ignore")
|
644
|
+
for x in server.get_flags(msgid)[msgid]
|
645
|
+
]
|
646
|
+
except Exception:
|
647
|
+
LOG.error("Failed to get flags.")
|
648
|
+
break
|
649
|
+
|
650
|
+
if "APRS" not in taglist:
|
651
|
+
# if msg not flagged as sent via aprs
|
652
|
+
try:
|
653
|
+
server.fetch([msgid], ["RFC822"])
|
654
|
+
except Exception:
|
655
|
+
LOG.exception("Failed single server fetch for RFC822")
|
656
|
+
break
|
657
|
+
|
658
|
+
(body, from_addr) = parse_email(msgid, data, server)
|
659
|
+
# unset seen flag, will stay bold in email client
|
660
|
+
try:
|
661
|
+
server.remove_flags(msgid, [imapclient.SEEN])
|
662
|
+
except Exception:
|
663
|
+
LOG.exception("Failed to remove flags SEEN")
|
664
|
+
# Not much we can do here, so lets try and
|
665
|
+
# send the aprs message anyway
|
666
|
+
|
667
|
+
if from_addr in shortcuts_inverted:
|
668
|
+
# reverse lookup of a shortcut
|
669
|
+
from_addr = shortcuts_inverted[from_addr]
|
670
|
+
|
671
|
+
reply = "-" + from_addr + " " + body.decode(errors="ignore")
|
672
|
+
# Send the message to the registered user in the
|
673
|
+
# config ham.callsign
|
674
|
+
tx.send(
|
675
|
+
packets.MessagePacket(
|
676
|
+
from_call=CONF.callsign,
|
677
|
+
to_call=CONF.email_plugin.callsign,
|
678
|
+
message_text=reply,
|
679
|
+
),
|
680
|
+
)
|
681
|
+
# flag message as sent via aprs
|
682
|
+
try:
|
683
|
+
server.add_flags(msgid, ["APRS"])
|
684
|
+
# unset seen flag, will stay bold in email client
|
685
|
+
except Exception:
|
686
|
+
LOG.exception("Couldn't add APRS flag to email")
|
687
|
+
|
688
|
+
try:
|
689
|
+
server.remove_flags(msgid, [imapclient.SEEN])
|
690
|
+
except Exception:
|
691
|
+
LOG.exception("Couldn't remove seen flag from email")
|
692
|
+
|
693
|
+
# check email more often since we just received an email
|
694
|
+
EmailInfo().delay = 60
|
695
|
+
|
696
|
+
# reset clock
|
697
|
+
LOG.debug("Done looping over Server.fetch, log out.")
|
698
|
+
self.past = datetime.datetime.now()
|
699
|
+
try:
|
700
|
+
server.logout()
|
701
|
+
except Exception:
|
702
|
+
LOG.exception("IMAP failed to logout: ")
|
703
|
+
return True
|
704
|
+
else:
|
705
|
+
# We haven't hit the email delay yet.
|
706
|
+
# LOG.debug("Delta({}) < {}".format(now - past, check_email_delay))
|
707
|
+
return True
|
708
|
+
|
709
|
+
return True
|