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.
Files changed (140) hide show
  1. aprsd/__init__.py +6 -4
  2. aprsd/cli_helper.py +151 -0
  3. aprsd/client/__init__.py +13 -0
  4. aprsd/client/aprsis.py +132 -0
  5. aprsd/client/base.py +105 -0
  6. aprsd/client/drivers/__init__.py +0 -0
  7. aprsd/client/drivers/aprsis.py +228 -0
  8. aprsd/client/drivers/fake.py +73 -0
  9. aprsd/client/drivers/kiss.py +119 -0
  10. aprsd/client/factory.py +88 -0
  11. aprsd/client/fake.py +48 -0
  12. aprsd/client/kiss.py +103 -0
  13. aprsd/client/stats.py +38 -0
  14. aprsd/cmds/__init__.py +0 -0
  15. aprsd/cmds/completion.py +22 -0
  16. aprsd/cmds/dev.py +162 -0
  17. aprsd/cmds/fetch_stats.py +156 -0
  18. aprsd/cmds/healthcheck.py +86 -0
  19. aprsd/cmds/list_plugins.py +319 -0
  20. aprsd/cmds/listen.py +231 -0
  21. aprsd/cmds/send_message.py +171 -0
  22. aprsd/cmds/server.py +137 -0
  23. aprsd/cmds/webchat.py +674 -0
  24. aprsd/conf/__init__.py +56 -0
  25. aprsd/conf/client.py +131 -0
  26. aprsd/conf/common.py +301 -0
  27. aprsd/conf/log.py +65 -0
  28. aprsd/conf/opts.py +80 -0
  29. aprsd/conf/plugin_common.py +182 -0
  30. aprsd/conf/plugin_email.py +105 -0
  31. aprsd/exception.py +13 -0
  32. aprsd/log/__init__.py +0 -0
  33. aprsd/log/log.py +138 -0
  34. aprsd/main.py +104 -867
  35. aprsd/packets/__init__.py +20 -0
  36. aprsd/packets/collector.py +79 -0
  37. aprsd/packets/core.py +823 -0
  38. aprsd/packets/log.py +161 -0
  39. aprsd/packets/packet_list.py +110 -0
  40. aprsd/packets/seen_list.py +49 -0
  41. aprsd/packets/tracker.py +103 -0
  42. aprsd/packets/watch_list.py +119 -0
  43. aprsd/plugin.py +474 -284
  44. aprsd/plugin_utils.py +86 -0
  45. aprsd/plugins/__init__.py +0 -0
  46. aprsd/plugins/email.py +709 -0
  47. aprsd/plugins/fortune.py +61 -0
  48. aprsd/plugins/location.py +179 -0
  49. aprsd/plugins/notify.py +61 -0
  50. aprsd/plugins/ping.py +31 -0
  51. aprsd/plugins/time.py +115 -0
  52. aprsd/plugins/version.py +31 -0
  53. aprsd/plugins/weather.py +405 -0
  54. aprsd/stats/__init__.py +20 -0
  55. aprsd/stats/app.py +49 -0
  56. aprsd/stats/collector.py +37 -0
  57. aprsd/threads/__init__.py +11 -0
  58. aprsd/threads/aprsd.py +119 -0
  59. aprsd/threads/keep_alive.py +131 -0
  60. aprsd/threads/log_monitor.py +121 -0
  61. aprsd/threads/registry.py +56 -0
  62. aprsd/threads/rx.py +354 -0
  63. aprsd/threads/stats.py +44 -0
  64. aprsd/threads/tx.py +255 -0
  65. aprsd/utils/__init__.py +218 -0
  66. aprsd/utils/counter.py +51 -0
  67. aprsd/utils/json.py +80 -0
  68. aprsd/utils/objectstore.py +123 -0
  69. aprsd/utils/ring_buffer.py +40 -0
  70. aprsd/utils/trace.py +180 -0
  71. aprsd/web/__init__.py +0 -0
  72. aprsd/web/admin/__init__.py +0 -0
  73. aprsd/web/admin/static/css/index.css +84 -0
  74. aprsd/web/admin/static/css/prism.css +4 -0
  75. aprsd/web/admin/static/css/tabs.css +35 -0
  76. aprsd/web/admin/static/images/Untitled.png +0 -0
  77. aprsd/web/admin/static/images/aprs-symbols-16-0.png +0 -0
  78. aprsd/web/admin/static/images/aprs-symbols-16-1.png +0 -0
  79. aprsd/web/admin/static/images/aprs-symbols-64-0.png +0 -0
  80. aprsd/web/admin/static/images/aprs-symbols-64-1.png +0 -0
  81. aprsd/web/admin/static/images/aprs-symbols-64-2.png +0 -0
  82. aprsd/web/admin/static/js/charts.js +235 -0
  83. aprsd/web/admin/static/js/echarts.js +465 -0
  84. aprsd/web/admin/static/js/logs.js +26 -0
  85. aprsd/web/admin/static/js/main.js +231 -0
  86. aprsd/web/admin/static/js/prism.js +12 -0
  87. aprsd/web/admin/static/js/send-message.js +114 -0
  88. aprsd/web/admin/static/js/tabs.js +28 -0
  89. aprsd/web/admin/templates/index.html +196 -0
  90. aprsd/web/chat/static/css/chat.css +115 -0
  91. aprsd/web/chat/static/css/index.css +66 -0
  92. aprsd/web/chat/static/css/style.css.map +1 -0
  93. aprsd/web/chat/static/css/tabs.css +41 -0
  94. aprsd/web/chat/static/css/upstream/bootstrap.min.css +6 -0
  95. aprsd/web/chat/static/css/upstream/font.woff2 +0 -0
  96. aprsd/web/chat/static/css/upstream/google-fonts.css +23 -0
  97. aprsd/web/chat/static/css/upstream/jquery-ui.css +1311 -0
  98. aprsd/web/chat/static/css/upstream/jquery.toast.css +28 -0
  99. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff +0 -0
  100. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff2 +0 -0
  101. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff +0 -0
  102. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff2 +0 -0
  103. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff +0 -0
  104. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff2 +0 -0
  105. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff +0 -0
  106. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff2 +0 -0
  107. aprsd/web/chat/static/images/Untitled.png +0 -0
  108. aprsd/web/chat/static/images/aprs-symbols-16-0.png +0 -0
  109. aprsd/web/chat/static/images/aprs-symbols-16-1.png +0 -0
  110. aprsd/web/chat/static/images/aprs-symbols-64-0.png +0 -0
  111. aprsd/web/chat/static/images/aprs-symbols-64-1.png +0 -0
  112. aprsd/web/chat/static/images/aprs-symbols-64-2.png +0 -0
  113. aprsd/web/chat/static/images/globe.svg +3 -0
  114. aprsd/web/chat/static/js/gps.js +84 -0
  115. aprsd/web/chat/static/js/main.js +45 -0
  116. aprsd/web/chat/static/js/send-message.js +585 -0
  117. aprsd/web/chat/static/js/tabs.js +28 -0
  118. aprsd/web/chat/static/js/upstream/bootstrap.bundle.min.js +7 -0
  119. aprsd/web/chat/static/js/upstream/jquery-3.7.1.min.js +2 -0
  120. aprsd/web/chat/static/js/upstream/jquery-ui.min.js +13 -0
  121. aprsd/web/chat/static/js/upstream/jquery.toast.js +374 -0
  122. aprsd/web/chat/static/js/upstream/semantic.min.js +11 -0
  123. aprsd/web/chat/static/js/upstream/socket.io.min.js +7 -0
  124. aprsd/web/chat/templates/index.html +139 -0
  125. aprsd/wsgi.py +315 -0
  126. aprsd-3.4.2.dist-info/AUTHORS +13 -0
  127. aprsd-3.4.2.dist-info/LICENSE +175 -0
  128. aprsd-3.4.2.dist-info/METADATA +793 -0
  129. aprsd-3.4.2.dist-info/RECORD +133 -0
  130. {aprsd-1.0.0.dist-info → aprsd-3.4.2.dist-info}/WHEEL +1 -1
  131. aprsd-3.4.2.dist-info/entry_points.txt +8 -0
  132. aprsd/fake_aprs.py +0 -83
  133. aprsd/utils.py +0 -166
  134. aprsd-1.0.0.dist-info/AUTHORS +0 -6
  135. aprsd-1.0.0.dist-info/METADATA +0 -181
  136. aprsd-1.0.0.dist-info/RECORD +0 -13
  137. aprsd-1.0.0.dist-info/entry_points.txt +0 -4
  138. aprsd-1.0.0.dist-info/pbr.json +0 -1
  139. /aprsd/{fuzzyclock.py → utils/fuzzyclock.py} +0 -0
  140. {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