Skip to content

Commit 92965f2

Browse files
authored
Merge pull request #190 from grillazz/171-simple-and-fast-smtp-client
171 simple and fast smtp client
2 parents 9d08ae9 + 189158d commit 92965f2

File tree

3 files changed

+82
-1
lines changed

3 files changed

+82
-1
lines changed

app/config.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
import os
22

3-
from pydantic import PostgresDsn, RedisDsn, computed_field
3+
from pydantic import PostgresDsn, RedisDsn, computed_field, BaseModel
44
from pydantic_core import MultiHostUrl
55
from pydantic_settings import BaseSettings, SettingsConfigDict
66

77

8+
class SMTPConfig(BaseModel):
9+
server: str = os.getenv("EMAIL_HOST", "smtp_server")
10+
port: int = os.getenv("EMAIL_PORT", 587)
11+
username: str = os.getenv("EMAIL_HOST_USER", "smtp_user")
12+
password: str = os.getenv("EMAIL_HOST_PASSWORD", "smtp_password")
13+
14+
815
class Settings(BaseSettings):
916
model_config = SettingsConfigDict(
1017
env_file=".env", env_ignore_empty=True, extra="ignore"
1118
)
1219
jwt_algorithm: str = os.getenv("JWT_ALGORITHM")
1320
jwt_expire: int = os.getenv("JWT_EXPIRE")
1421

22+
smtp: SMTPConfig = SMTPConfig()
23+
1524
REDIS_HOST: str
1625
REDIS_PORT: int
1726
REDIS_DB: str

app/services/smtp.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import smtplib
2+
from email.mime.multipart import MIMEMultipart
3+
from email.mime.text import MIMEText
4+
5+
from app.config import settings as global_settings
6+
7+
from fastapi.templating import Jinja2Templates
8+
9+
from pydantic import EmailStr
10+
11+
from app.utils.logging import AppLogger
12+
from app.utils.singleton import SingletonMetaNoArgs
13+
14+
15+
logger = AppLogger().get_logger()
16+
17+
18+
class SMTPEmailService(metaclass=SingletonMetaNoArgs):
19+
def __init__(self):
20+
self.server = smtplib.SMTP(
21+
global_settings.smtp.server, global_settings.smtp.port
22+
)
23+
self.server.starttls()
24+
self.server.login(global_settings.smtp.username, global_settings.smtp.password)
25+
self.templates = Jinja2Templates("templates")
26+
27+
def send_email(
28+
self,
29+
sender: EmailStr,
30+
recipients: list[EmailStr],
31+
subject: str,
32+
body_text: str = "",
33+
body_html=None,
34+
):
35+
msg = MIMEMultipart()
36+
msg["From"] = sender
37+
msg["To"] = ",".join(recipients)
38+
msg["Subject"] = subject
39+
msg.attach(MIMEText(body_text, "plain"))
40+
if body_html:
41+
msg.attach(MIMEText(body_html, "html"))
42+
self.server.sendmail(sender, recipients, msg.as_string())
43+
44+
def send_template_email(
45+
self,
46+
recipients: list[EmailStr],
47+
subject: str,
48+
template: str = None,
49+
context: dict = None,
50+
sender: EmailStr = global_settings.smtp.from_email,
51+
):
52+
template_str = self.templates.get_template(template)
53+
body_html = template_str.render(context)
54+
self.send_email(sender, recipients, subject, body_html=body_html)

app/utils/singleton.py

+18
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,21 @@ def __call__(cls, *args, **kwargs):
1616
instance = super().__call__(*args, **kwargs)
1717
cls._instances[cls] = instance
1818
return cls._instances[cls]
19+
20+
21+
class SingletonMetaNoArgs(type):
22+
"""
23+
Singleton metaclass for classes without parameters on constructor,
24+
for compatibility with FastApi Depends() function.
25+
"""
26+
27+
_instances = {}
28+
29+
_lock: Lock = Lock()
30+
31+
def __call__(cls):
32+
with cls._lock:
33+
if cls not in cls._instances:
34+
instance = super().__call__()
35+
cls._instances[cls] = instance
36+
return cls._instances[cls]

0 commit comments

Comments
 (0)