Coverage for chalicelib/utils/smtp.py: 39%
83 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:56 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:56 +0000
1import logging
2import smtplib
3from smtplib import SMTPAuthenticationError
5from decouple import config
6from fastapi import HTTPException
8logger = logging.getLogger(__name__)
10class EmptySMTP:
11 def sendmail(self, from_addr, to_addrs, msg, mail_options=(), rcpt_options=()):
12 logger.error("!! CANNOT SEND EMAIL, NO VALID SMTP CONFIGURATION FOUND")
14 def send_message(self, msg):
15 self.sendmail( msg["FROM"], msg["TO"], msg.as_string() )
17class SMTPClient:
18 server = None
20 def __init__(self):
21 if config("EMAIL_HOST") is None or len(config("EMAIL_HOST")) == 0: 21 ↛ 23line 21 didn't jump to line 23 because the condition on line 21 was always true
22 return
23 elif not config("EMAIL_USE_SSL", cast=bool):
24 self.server = smtplib.SMTP(host=config("EMAIL_HOST"), port=config("EMAIL_PORT", cast=int))
25 else:
26 if len(config("EMAIL_SSL_KEY")) == 0 or len(config("EMAIL_SSL_CERT")) == 0:
27 self.server = smtplib.SMTP_SSL(host=config("EMAIL_HOST"), port=config("EMAIL_PORT", cast=int))
28 else:
29 self.server = smtplib.SMTP_SSL(host=config("EMAIL_HOST"), port=config("EMAIL_PORT", cast=int),
30 keyfile=config("EMAIL_SSL_KEY"), certfile=config("EMAIL_SSL_CERT"))
32 def __enter__(self):
33 if self.server is None: 33 ↛ 35line 33 didn't jump to line 35 because the condition on line 33 was always true
34 return EmptySMTP()
35 self.server.ehlo()
36 if not config("EMAIL_USE_SSL", cast=bool) and config("EMAIL_USE_TLS", cast=bool):
37 self.server.starttls()
38 # stmplib docs recommend calling ehlo() before & after starttls()
39 self.server.ehlo()
40 if len(config("EMAIL_USER", default="")) > 0 and len(config("EMAIL_PASSWORD", default="")) > 0:
41 try:
42 self.server.login(user=config("EMAIL_USER"), password=config("EMAIL_PASSWORD"))
43 except SMTPAuthenticationError:
44 raise HTTPException(401, "SMTP Authentication Error")
45 return self.server
47 def __exit__(self, *args):
48 if self.server is None: 48 ↛ 50line 48 didn't jump to line 50 because the condition on line 48 was always true
49 return
50 self.server.quit()
52 def test_configuration(self):
53 # check server connexion
54 try:
55 status = self.server.noop()[0]
56 if not (status == 250):
57 raise Exception(f"SMTP connexion error, status:{status}")
58 except Exception as e: # smtplib.SMTPServerDisconnected
59 logger.error(
60 f'!! SMTP connexion error to {config("EMAIL_HOST")}:{config("EMAIL_PORT", cast=int)}')
61 logger.error(e)
62 return False, e
64 # check authentication
65 try:
66 self.__enter__()
67 self.__exit__()
68 except Exception as e:
69 logger.error(f'!! SMTP authentication error to {config("EMAIL_HOST")}:{config("EMAIL_PORT", cast=int)}')
70 logger.error(e)
71 return False, e
73 return True, None
76VALID_SMTP = None
77SMTP_ERROR = None
78SMTP_NOTIFIED = False
81def has_smtp():
82 global VALID_SMTP, SMTP_ERROR, SMTP_NOTIFIED
83 if SMTP_ERROR is not None: 83 ↛ 84line 83 didn't jump to line 84 because the condition on line 83 was never true
84 logger.error("!!! SMTP error found, disabling SMTP configuration:")
85 logger.error(SMTP_ERROR)
87 if VALID_SMTP is not None: 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true
88 return VALID_SMTP
90 if config("EMAIL_HOST") is not None and len(config("EMAIL_HOST")) > 0: 90 ↛ 91line 90 didn't jump to line 91 because the condition on line 90 was never true
91 VALID_SMTP, SMTP_ERROR = check_connexion()
92 return VALID_SMTP
93 elif not SMTP_NOTIFIED:
94 SMTP_NOTIFIED = True
95 logger.info("no SMTP configuration found")
96 return False
99def check_connexion():
100 # check SMTP host&port
101 import socket
102 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
103 sock.settimeout(config("EMAIL_CHECK_TIMEOUT", cast=int, default=5))
104 result = sock.connect_ex((config("EMAIL_HOST"), config("EMAIL_PORT", cast=int)))
105 sock.close()
106 if not (result == 0):
107 error = f"""!! SMTP {config("EMAIL_HOST")}:{config("EMAIL_PORT", cast=int)} is unreachable
108f'please make sure the host&port are correct, and the SMTP protocol is authorized on your server."""
109 logger.error(error)
110 sock.close()
111 return False, error
113 return SMTPClient().test_configuration()