I'm using fastapi-mail
package, and trying to send multiple files to multiple email addresses. When I send the email to only one email address, the application works as expected. However, when I change to List[EmailStr]
for sending to multiple email addresses, I get this error:
not a valid email address
Here is my code:
@app.post("/file")async def send_file(
background_tasks: BackgroundTasks,
email:List[EmailStr] = Form(...), #I Change here before EmailStr = Form(...)
file:Optional[List[UploadFile]] = File(...),) -> JSONResponse:
print(email)
print(file)
message = MessageSchema(
subject="Fastapi mail module",
recipients=email,
body="Simple background task",
subtype="html",
attachments=file)
fm = FastMail(ConnectionConfig(
MAIL_USERNAME=res("MAIL_USERNAME"),
MAIL_PASSWORD=res("MAIL_PASSWORD"),
MAIL_FROM="admin@acsebs.com",
MAIL_PORT=res("MAIL_PORT"),
MAIL_SERVER=res("MAIL_SERVER"),
MAIL_FROM_NAME="send attachment email service",
MAIL_TLS=res("MAIL_TLS"),
MAIL_SSL=res("MAIL_SSL"),
USE_CREDENTIALS=res("USE_CREDENTIALS"),
VALIDATE_CERTS=res("VALIDATE_CERTS")
))
background_tasks.add_task(fm.send_message, message)
return JSONResponse(status_code=200, content={"message": "email has been sent"})
The issue is not with your code, but with Swagger UI when sending multiple values for the same field. As described in this answer, Swagger UI incorrectly adds all items as a single item to the list, separated by comma (you can confirm that by looking at the second screenshot you provided, under the "Curl" section). For example, when you pass two or more email addresses to your endpoint through Swagger UI, they are sent as:
['user1@example.com, user2@example.com']
instead of:
['user1@example.com', 'user2@example.com']
Hence, the error is raised, as 'user1@example.com, user2@example.com'
(all together as a single string) is not a valid email address. If you sent a request using HTML <form>
or JavaScript fetch
—similar to Method 1 and Method 3 of this answer—you would see that your code would work just fine.
Note 1: Use a different <input>
element for each email
address, but use the same name
value for all (i.e., emails
, which is the name of the parameter defined in the endpoint).
Note 2: On a side note, be aware that the "the most important part to make a parameter Optional
is the part = None
", as described in this answer and this comment. You seem to have defined your files
parameter in your endpoint with the Optional
keyword, but using = File(...)
or ignoring that part at all, would make files
a required field; hence, make sure to use = File(None)
, if you want it to be optional instead.
@app.post("/email")
def send_email(emails: List[EmailStr] = Form(...),
files: Optional[List[UploadFile]] = File(None)):
return emails
@app.get('/emailForm', response_class=HTMLResponse)
def index():
return """
<html>
<body>
<form method="POST" action="/email" enctype="multipart/form-data">
<label for="email1">Email 1:</label>
<input type="text" id="email1" name="emails"><br><br>
<label for="email2">Email 2:</label>
<input type="text" id="email2" name="emails"><br><br>
<input type="file" id="files" name="files" multiple>
<input type="submit" value="Submit">
</form>
</body>
</html>
"""
If you need to use Swagger UI and would like your endpoint to work when submitting requests through there as well, here is a solution, as suggested here. Perform a check on the length
of the emails list, and if it is equal to 1
(meaning that the list contains a single item), then split this item using the comma delimiter to get the actual list of email addresses. Finally, loop through the list to validate each email using the email-validator
, which is used by Pydantic behind the scenes.
from fastapi import FastAPI, Depends, UploadFile, File, Form, HTTPException, status
from email_validator import validate_email, EmailNotValidError
from typing import List, Optional
def check_email(email: str):
try:
validation = validate_email(email, check_deliverability=False)
return validation.email
except EmailNotValidError as e:
raise HTTPException(detail=f"'{email}' is not a valid email address. {str(e)}",
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
def email_checker(emails: List[str] = Form(...)):
if len(emails) == 1:
emails = [item.strip() for item in emails[0].split(',')]
return [check_email(email) for email in emails]
@app.post("/email")
def send_email(emails: List[str] = Depends(email_checker)):
return emails