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