0

I'm trying to creating the User Authentication API using FastAPI. My scope is when a new user gets registered via endpoint an email will be sent to verify the JWT token and after verification user will be added to the database. Issue I'm facing to reach to the end is this smtplib.SMTPSenderRefused: (530, b'5.7.0 Must issue a STARTTLS command first whenever control reach to Mailer class API crashed.I tried enabling TLS in my config file but no luck so far.

Here is my config:

EMAIL_HOST_USER ='noreply@gmail.com'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_PASSWORD = 'my_password'
EMAIL_USE_TLS = True

Mailer_Class:

from smtplib import SMTP
import smtplib
from email.message import EmailMessage
from core.config import Settings
 
settings = Settings()
 
 
class Mailer:
    @staticmethod
    def send_message(content: str, subject: str, mail_to: str):
        message = EmailMessage()
        message.set_content(content)
        message['Subject'] = subject
        message['From'] = settings.EMAIL_HOST_USER
        message['To'] = mail_to
        SMTP.ehlo()
        SMTP.starttls()
        SMTP.ehlo()
        SMTP.login(settings.EMAIL_HOST_USER, settings.EMAIL_HOST_PASSWORD)
        SMTP(settings.EMAIL_HOST).send_message(message)
 
 
 
    @staticmethod
    def send_confirmation_message(token: str, mail_to: str):
        confirmation_url = f'localhost:8000/registration/verify/{token}'
        message = '''Hi!
    Please confirm your registration: {}.'''.format(confirmation_url)
        Mailer.send_message(
            message,
            'Please confirm your registration',
            mail_to
        )

Traceback:

INFO:     127.0.0.1:63757 - "POST /registration/?username=string&email=string%40string.com&password=srinf12 HTTP/1.1" 500 Internal Server Error
2021-07-13 22:54:04,745 INFO sqlalchemy.engine.Engine ROLLBACK
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "c:\users\xone\desktop\fastapi-job-board\job_board\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 369, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "c:\users\xone\desktop\fastapi-job-board\job_board\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 59, in __call__
    return await self.app(scope, receive, send)
  File "c:\users\xone\desktop\fastapi-job-board\job_board\lib\site-packages\fastapi\applications.py", line 199, in __call__
    await super().__call__(scope, receive, send)
  File "c:\users\xone\desktop\fastapi-job-board\job_board\lib\site-packages\starlette\applications.py", line 112, in __call__       
    await self.middleware_stack(scope, receive, send)
  File "c:\users\xone\desktop\fastapi-job-board\job_board\lib\site-packages\starlette\middleware\errors.py", line 181, in __call__  
    raise exc from None
  File "c:\users\xone\desktop\fastapi-job-board\job_board\lib\site-packages\starlette\middleware\errors.py", line 159, in __call__  
    await self.app(scope, receive, _send)
  File "c:\users\xone\desktop\fastapi-job-board\job_board\lib\site-packages\starlette\exceptions.py", line 82, in __call__
    raise exc from None
  File "c:\users\xone\desktop\fastapi-job-board\job_board\lib\site-packages\starlette\exceptions.py", line 71, in __call__
    await self.app(scope, receive, sender)
  File "c:\users\xone\desktop\fastapi-job-board\job_board\lib\site-packages\starlette\routing.py", line 580, in __call__
    await route.handle(scope, receive, send)
  File "c:\users\xone\desktop\fastapi-job-board\job_board\lib\site-packages\starlette\routing.py", line 241, in handle
    await self.app(scope, receive, send)
  File "c:\users\xone\desktop\fastapi-job-board\job_board\lib\site-packages\starlette\routing.py", line 52, in app
    response = await func(request)
  File "c:\users\xone\desktop\fastapi-job-board\job_board\lib\site-packages\fastapi\routing.py", line 216, in app
    raw_response = await run_endpoint_function(
  File "c:\users\xone\desktop\fastapi-job-board\job_board\lib\site-packages\fastapi\routing.py", line 149, in run_endpoint_function 
    return await dependant.call(**values)
  File ".\routes\route_user.py", line 32, in create_user
    Mailer.send_confirmation_message(confirmation["token"], form_data.email)
  File ".\core\mailer.py", line 26, in send_confirmation_message
    Mailer.send_message(
  File ".\core\mailer.py", line 17, in send_message
    SMTP(settings.EMAIL_HOST).send_message(message)
  File "C:\Python\Python38\lib\smtplib.py", line 970, in send_message
    return self.sendmail(from_addr, to_addrs, flatmsg, mail_options,
  File "C:\Python\Python38\lib\smtplib.py", line 871, in sendmail
    raise SMTPSenderRefused(code, resp, from_addr)
smtplib.SMTPSenderRefused: (530, b'5.7.0 Must issue a STARTTLS command first. r18sm11961741wrt.96 - gsmtp', 'noreply@gmail.com')

When I try with lower level smtp functions:

SMTP.ehlo()
SMTP.starttls()
SMTP.ehlo()
SMTP.login(settings.EMAIL_HOST_USER, settings.EMAIL_HOST_PASSWORD)

Traceback:

TypeError: ehlo() missing 1 required positional argument: 'self'
dougj
  • 135
  • 3
  • 15
  • 1
    AFAIK, in this case you have to use the lower level SMTP functions: `ehlo`, `starttls`, `ehlo` (again), `login` with your credentials, and then finally send your message – VPfB Jul 13 '21 at 19:27
  • @VPfB I tried with the lower level functions of smtp as well but it giving me required positional argument error. Which kind of arguments does these functions accept? I updated the code as well. – dougj Jul 13 '21 at 19:41
  • 1
    See the relevant code fragment in this answer: https://stackoverflow.com/a/27515833/5378816 and of course, read the smtplib docs. – VPfB Jul 13 '21 at 19:57
  • 1
    @tripleee the `ehlo` has a default, I thnik the error is caused by calling the unbound class method. – VPfB Jul 13 '21 at 20:06
  • It turns out I was accessing these low level functions directly without instantiating the smtp class so that is why it was giving that positional argument error. Anyhow Mailer is working now Thanks guys. – dougj Jul 13 '21 at 20:13

1 Answers1

0

I faced the same problem when sending emails from FastAPI. This is what I currently use to send email, hope this helps:

import logging
import emails
from .validation import get_time_utc
from emails.template import JinjaTemplate
from fastapi import HTTPException
from models.aws_checker import Mailer
from starlette.responses import JSONResponse
from starlette.status import HTTP_406_NOT_ACCEPTABLE
from .const import (EMAIL_ACCESS_SUBJECT, EMAIL_ASSIGNED_TO,
                    EMAIL_EXCEPTION_SUBJECT, EMAIL_PASSWORD, EMAIL_SENDER)
from .aws_s3 import write_to_s3

resp_time = get_time_utc()

html_template = """
<!DOCTYPE html>
<html lang="en">
    <body>
        <p><i>(This is an automated service email)</i></p>
        <h2>
            Request Summary
        </h2>
        Requestor's Email: <b>{{ email }}</b><br/>
        Resource Name: <b>{{ resource_name }}</b><br/>
        Duration: <b>{{ duration }}</b><br/>
        Comments: <b>{{ comments }}</b><br/>
    </body>
    <h1></h1>
    <h1></h1>
    <img src="https://test.png" alt="test" style="width:10%">
    <style>
        img {
            display: block;
            margin-left: auto;
            margin-right: auto;
        }
    </style>
</html>
"""


async def send_email(environment: Mailer) -> JSONResponse:
    if environment.type == "access":
        subject = EMAIL_ACCESS_SUBJECT
        policy_path = resp_time.strftime("policy_%H_%M_%S")
        s3_path = environment.comments
        file_path = await write_to_s3(file_content=environment.comments,
                                      accountid=environment.accountid,
                                      requestors_email=environment.email)
        s3_path_hyperlink = '<a href="' + file_path + '"' + '>S3 Policy location</a>'
        print("%" * 50)
        print(s3_path_hyperlink)
        comments = s3_path_hyperlink
    elif environment.type == "notify":
        subject = EMAIL_ACCESS_SUBJECT
        comments = environment.comments
    else:
        subject = EMAIL_EXCEPTION_SUBJECT
        comments = environment.comments
    env = {
        "email": environment.email,
        "accountid": environment.accountid,
        "duration": environment.duration,
        "resource_name": environment.resource_name,
        "comments": comments
        }
    message = emails.Message(
        subject=subject,
        html=JinjaTemplate(html_template),
        mail_from=EMAIL_SENDER
    )
    smtp_options = {
        "host": 'smtp.gmail.com',
        "port": 465,
        "ssl": True,
        "user": EMAIL_SENDER,
        "password": EMAIL_PASSWORD
        }
    try:
        response = message.send(to=[EMAIL_ASSIGNED_TO, environment.email], render=env, smtp=smtp_options)
        logging.info(f"send email result: {response}")
        return JSONResponse(status_code=250, content={"message": "email has been sent"})
    except:
        raise HTTPException(status_code=HTTP_406_NOT_ACCEPTABLE)
tomarv2
  • 753
  • 3
  • 14
  • Thank you for sharing your useful knowledge but it turns out I was accessing these low level functions directly without instantiating the smtp class so that is why it was giving that positional argument error. Anyhow Mailer is working now. – dougj Jul 13 '21 at 20:14