mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
add smtp support fixes #64
This commit is contained in:
parent
82e00db890
commit
c8af2120e2
5 changed files with 251 additions and 48 deletions
|
@ -94,7 +94,7 @@ def create_app(config_class=Config):
|
|||
app.jinja_env.globals['get_resource_as_string'] = get_resource_as_string
|
||||
|
||||
# send error reports via email
|
||||
if app.config['MAIL_SERVER']:
|
||||
if app.config['MAIL_SERVER'] and app.config['MAIL_ERRORS']:
|
||||
auth = None
|
||||
if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
|
||||
auth = (app.config['MAIL_USERNAME'],
|
||||
|
|
262
app/email.py
262
app/email.py
|
@ -4,8 +4,10 @@ from flask_babel import _, lazy_gettext as _l # todo: set the locale based on a
|
|||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
from typing import List
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
AWS_REGION = "ap-southeast-2"
|
||||
CHARSET = "UTF-8"
|
||||
|
||||
|
||||
|
@ -48,51 +50,61 @@ def send_async_email(subject, sender, recipients, text_body, html_body, reply_to
|
|||
if type(recipients) == str:
|
||||
recipients = [recipients]
|
||||
with current_app.app_context():
|
||||
try:
|
||||
# Create a new SES resource and specify a region.
|
||||
amazon_client = boto3.client('ses', region_name=AWS_REGION)
|
||||
# Provide the contents of the email.
|
||||
if reply_to is None:
|
||||
response = amazon_client.send_email(
|
||||
Destination={'ToAddresses': recipients},
|
||||
Message={
|
||||
'Body': {
|
||||
'Html': {
|
||||
'Charset': CHARSET, 'Data': html_body,
|
||||
if current_app.config['MAIL_SERVER']:
|
||||
email_sender = SMTPEmailService(current_app.config['MAIL_USERNAME'],
|
||||
current_app.config['MAIL_PASSWORD'],
|
||||
(current_app.config['MAIL_SERVER'], current_app.config['MAIL_PORT']),
|
||||
use_tls=current_app.config['MAIL_USE_TLS'])
|
||||
email_sender.set_message(text_body, subject, sender, html_body) # sender = 'John Doe <j.doe@server.com>'
|
||||
email_sender.set_recipients(recipients)
|
||||
email_sender.connect()
|
||||
email_sender.send_all(close_connection=True)
|
||||
elif current_app.config['AWS_REGION']:
|
||||
try:
|
||||
# Create a new SES resource and specify a region.
|
||||
amazon_client = boto3.client('ses', region_name=current_app.config['AWS_REGION'])
|
||||
# Provide the contents of the email.
|
||||
if reply_to is None:
|
||||
response = amazon_client.send_email(
|
||||
Destination={'ToAddresses': recipients},
|
||||
Message={
|
||||
'Body': {
|
||||
'Html': {
|
||||
'Charset': CHARSET, 'Data': html_body,
|
||||
},
|
||||
'Text': {
|
||||
'Charset': CHARSET, 'Data': text_body,
|
||||
},
|
||||
},
|
||||
'Text': {
|
||||
'Charset': CHARSET, 'Data': text_body,
|
||||
'Subject': {
|
||||
'Charset': CHARSET, 'Data': subject,
|
||||
},
|
||||
},
|
||||
'Subject': {
|
||||
'Charset': CHARSET, 'Data': subject,
|
||||
},
|
||||
},
|
||||
Source=sender,
|
||||
ReturnPath=return_path)
|
||||
else:
|
||||
response = amazon_client.send_email(
|
||||
Destination={'ToAddresses': recipients},
|
||||
Message={
|
||||
'Body': {
|
||||
'Html': {
|
||||
'Charset': CHARSET, 'Data': html_body,
|
||||
Source=sender,
|
||||
ReturnPath=return_path)
|
||||
else:
|
||||
response = amazon_client.send_email(
|
||||
Destination={'ToAddresses': recipients},
|
||||
Message={
|
||||
'Body': {
|
||||
'Html': {
|
||||
'Charset': CHARSET, 'Data': html_body,
|
||||
},
|
||||
'Text': {
|
||||
'Charset': CHARSET, 'Data': text_body,
|
||||
},
|
||||
},
|
||||
'Text': {
|
||||
'Charset': CHARSET, 'Data': text_body,
|
||||
'Subject': {
|
||||
'Charset': CHARSET, 'Data': subject,
|
||||
},
|
||||
},
|
||||
'Subject': {
|
||||
'Charset': CHARSET, 'Data': subject,
|
||||
},
|
||||
},
|
||||
Source=sender,
|
||||
ReturnPath=return_path,
|
||||
ReplyToAddresses=[reply_to])
|
||||
# message.attach_alternative("...AMPHTML content...", "text/x-amp-html")
|
||||
except ClientError as e:
|
||||
current_app.logger.error('Failed to send email. ' + e.response['Error']['Message'])
|
||||
return e.response['Error']['Message']
|
||||
Source=sender,
|
||||
ReturnPath=return_path,
|
||||
ReplyToAddresses=[reply_to])
|
||||
# message.attach_alternative("...AMPHTML content...", "text/x-amp-html")
|
||||
except ClientError as e:
|
||||
current_app.logger.error('Failed to send email. ' + e.response['Error']['Message'])
|
||||
return e.response['Error']['Message']
|
||||
|
||||
|
||||
def send_email(subject, sender, recipients: List[str], text_body, html_body, reply_to=None):
|
||||
|
@ -100,3 +112,173 @@ def send_email(subject, sender, recipients: List[str], text_body, html_body, rep
|
|||
send_async_email(subject, sender, recipients, text_body, html_body, reply_to)
|
||||
else:
|
||||
send_async_email.delay(subject, sender, recipients, text_body, html_body, reply_to)
|
||||
|
||||
|
||||
class SMTPEmailService:
|
||||
"""
|
||||
Contains email contents, connection settings and recipient settings. Has functions to compose and send mail. MailSenders are tied to an SMTP server, which must be specified when the instance is created. The default SMTP server is Google's Gmail server, with a connection over TLS.
|
||||
:param in_username: Username for mail server login (required)
|
||||
:param in_password: Password for mail server login (required)
|
||||
:param in_server: SMTP server to connect to
|
||||
:param use_tls: Select whether to connect over SSL (False) or TLS (True). Keep in mind that SMTP servers use different ports for SSL and TLS.
|
||||
"""
|
||||
|
||||
def __init__(self, in_username, in_password, in_server, use_tls):
|
||||
self.username = in_username
|
||||
self.password = in_password
|
||||
self.server_name = in_server[0]
|
||||
self.server_port = in_server[1]
|
||||
self.use_tls = use_tls
|
||||
|
||||
if not self.use_tls:
|
||||
self.smtpserver = smtplib.SMTP_SSL(self.server_name, self.server_port)
|
||||
else:
|
||||
self.smtpserver = smtplib.SMTP(self.server_name, self.server_port)
|
||||
self.connected = False
|
||||
self.recipients = []
|
||||
|
||||
def __str__(self):
|
||||
return "Type: Mail Sender \n" \
|
||||
"Connection to server {}, port {} \n" \
|
||||
"Connected: {} \n" \
|
||||
"Username: {}, Password: {}".format(
|
||||
self.server_name, self.server_port, self.connected, self.username, self.password)
|
||||
|
||||
def set_message(self, plaintext, subject="", in_from=None, htmltext=None):
|
||||
"""
|
||||
Creates the MIME message to be sent by e-mail. Optionally allows adding subject and 'from' field. Sets up empty recipient fields. To use html messages specify an htmltext input
|
||||
:param plaintext: Plaintext email body (required even when HTML message is specified, as fallback)
|
||||
:param subject: Subject line (optional)
|
||||
:param in_from: Sender address (optional, whether this setting is copied by the SMTP server depends on the server's settings)
|
||||
:param htmltext: HTML version of the email body (optional) (If you want to use an HTML message but set it later, pass an empty string here)
|
||||
"""
|
||||
|
||||
if htmltext is not None:
|
||||
self.html_ready = True
|
||||
else:
|
||||
self.html_ready = False
|
||||
|
||||
if self.html_ready:
|
||||
# 'alternative' allows attaching an html version of the message later
|
||||
self.msg = MIMEMultipart('alternative')
|
||||
self.msg.attach(MIMEText(plaintext, 'plain'))
|
||||
self.msg.attach(MIMEText(htmltext, 'html'))
|
||||
else:
|
||||
self.msg = MIMEText(plaintext, 'plain')
|
||||
|
||||
self.msg['Subject'] = subject
|
||||
if in_from is None:
|
||||
self.msg['From'] = self.username
|
||||
else:
|
||||
self.msg['From'] = in_from
|
||||
self.msg["To"] = None
|
||||
self.msg["CC"] = None
|
||||
self.msg["BCC"] = None
|
||||
|
||||
def clear_message(self):
|
||||
"""
|
||||
Remove the whole email body. If both plaintext and html are attached both are removed
|
||||
"""
|
||||
self.msg.set_payload("")
|
||||
|
||||
def set_subject(self, in_subject):
|
||||
self.msg.replace_header("Subject", in_subject)
|
||||
|
||||
def set_from(self, in_from):
|
||||
self.msg.replace_header("From", in_from)
|
||||
|
||||
def set_plaintext(self, in_body_text):
|
||||
"""
|
||||
Set plaintext message: replaces entire payload if no html is used, otherwise replaces the plaintext only
|
||||
:param in_body_text: Plaintext email body, replaces old plaintext email body
|
||||
"""
|
||||
if not self.html_ready:
|
||||
self.msg.set_payload(in_body_text)
|
||||
else:
|
||||
payload = self.msg.get_payload()
|
||||
payload[0] = MIMEText(in_body_text)
|
||||
self.msg.set_payload(payload)
|
||||
|
||||
def set_html(self, in_html):
|
||||
"""
|
||||
Replace HTML version of the email body. The plaintext version is unaffected.
|
||||
:param in_html: HTML email body, replaces old HTML email body
|
||||
"""
|
||||
try:
|
||||
payload = self.msg.get_payload()
|
||||
payload[1] = MIMEText(in_html, 'html')
|
||||
self.msg.set_payload(payload)
|
||||
except TypeError:
|
||||
print("ERROR: "
|
||||
"Payload is not a list. Specify an HTML message with in_htmltext in MailSender.set_message()")
|
||||
raise
|
||||
|
||||
def set_recipients(self, in_recipients):
|
||||
"""
|
||||
Sets the list of recipients' email addresses. This is used by the email sending functions.
|
||||
:param in_recipients: All recipients to whom the email should be sent (Must be a list, even when there is only one recipient)
|
||||
"""
|
||||
if not isinstance(in_recipients, (list, tuple)):
|
||||
raise TypeError(
|
||||
"Recipients must be a list or tuple, is {}".format(type(in_recipients)))
|
||||
|
||||
self.recipients = in_recipients
|
||||
|
||||
def set_cc_bcc(self, cc, bcc):
|
||||
cc = []
|
||||
if self.msg.CC:
|
||||
if isinstance(self.msg.CC, str):
|
||||
cc = [self.msg.CC]
|
||||
else:
|
||||
cc = list(self.msg.CC)
|
||||
|
||||
bcc = []
|
||||
if self.msg.BCC:
|
||||
if isinstance(self.msg.BCC, str):
|
||||
bcc = [self.msg.BCC]
|
||||
else:
|
||||
bcc = list(self.msg.BCC)
|
||||
|
||||
self.recipients.append(cc)
|
||||
self.recipients.append(bcc)
|
||||
|
||||
def add_recipient(self, in_recipient):
|
||||
"""Adds a recipient to the back of the list
|
||||
:param in_recipient: Recipient email addresses
|
||||
"""
|
||||
self.recipients.append(in_recipient)
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Must be called before sending messages. Connects to SMTP server using the username and password.
|
||||
"""
|
||||
if self.use_tls:
|
||||
self.smtpserver.starttls()
|
||||
self.smtpserver.login(self.username, self.password)
|
||||
self.connected = True
|
||||
print("Connected to {}".format(self.server_name))
|
||||
|
||||
def disconnect(self):
|
||||
self.smtpserver.close()
|
||||
self.connected = False
|
||||
|
||||
def send_all(self, close_connection=True):
|
||||
"""Sends message to all specified recipients, one at a time. Optionally closes connection after sending. Close the connection after sending if you are not sending another batch of emails immediately after.
|
||||
:param close_connection: Should the connection to the server be closed after all emails have been sent (True) or not (False)
|
||||
"""
|
||||
if not self.connected:
|
||||
raise ConnectionError(
|
||||
"Not connected to any server. Try self.connect() first")
|
||||
|
||||
print("Message: {}".format(self.msg.get_payload()))
|
||||
|
||||
for recipient in self.recipients:
|
||||
self.msg.replace_header("To", recipient)
|
||||
print("Sending to {}".format(recipient))
|
||||
self.smtpserver.send_message(self.msg)
|
||||
|
||||
print("All messages sent")
|
||||
|
||||
if close_connection:
|
||||
self.disconnect()
|
||||
print("Connection closed")
|
||||
|
|
|
@ -17,7 +17,7 @@ from app.inoculation import inoculation
|
|||
from app.main import bp
|
||||
from flask import g, session, flash, request, current_app, url_for, redirect, make_response, jsonify
|
||||
from flask_moment import moment
|
||||
from flask_login import current_user
|
||||
from flask_login import current_user, login_required
|
||||
from flask_babel import _, get_locale
|
||||
from sqlalchemy import select, desc, text
|
||||
from sqlalchemy_searchable import search
|
||||
|
@ -308,6 +308,15 @@ def test():
|
|||
return 'ok'
|
||||
|
||||
|
||||
@bp.route('/test_email')
|
||||
@login_required
|
||||
def test_email():
|
||||
send_email('This is a test email', f'{g.site.name} <{current_app.config["MAIL_FROM"]}>', [current_user.email],
|
||||
'This is a test email. If you received this, email sending is working!',
|
||||
'<p>This is a test email. If you received this, email sending is working!</p>')
|
||||
return f'Email sent to {current_user.email}.'
|
||||
|
||||
|
||||
def verification_warning():
|
||||
if hasattr(current_user, 'verified') and current_user.verified is False:
|
||||
flash(_('Please click the link in your email inbox to verify your account.'), 'warning')
|
||||
|
|
11
config.py
11
config.py
|
@ -11,12 +11,13 @@ class Config(object):
|
|||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
|
||||
'sqlite:///' + os.path.join(basedir, 'app.db')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
||||
MAIL_SERVER = os.environ.get('MAIL_SERVER') or None
|
||||
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)
|
||||
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
|
||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
||||
MAIL_FROM = os.environ.get('MAIL_FROM')
|
||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or None
|
||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or None
|
||||
MAIL_FROM = os.environ.get('MAIL_FROM') or None
|
||||
MAIL_ERRORS = os.environ.get('MAIL_ERRORS') is not None
|
||||
ADMINS = os.environ.get('ADMINS')
|
||||
RECAPTCHA_PUBLIC_KEY = os.environ.get("RECAPTCHA_PUBLIC_KEY")
|
||||
RECAPTCHA_PRIVATE_KEY = os.environ.get("RECAPTCHA_PRIVATE_KEY")
|
||||
|
@ -39,3 +40,5 @@ class Config(object):
|
|||
BOUNCE_PASSWORD = os.environ.get('BOUNCE_PASSWORD') or ''
|
||||
|
||||
SENTRY_DSN = os.environ.get('SENTRY_DSN') or None
|
||||
|
||||
AWS_REGION = os.environ.get('AWS_REGION') or None
|
||||
|
|
13
env.sample
13
env.sample
|
@ -2,6 +2,13 @@ SECRET_KEY=
|
|||
SERVER_NAME='127.0.0.1:5000'
|
||||
DATABASE_URL=postgresql+psycopg2://pyfedi:pyfedi@127.0.0.1/pyfedi
|
||||
MAIL_SERVER=''
|
||||
MAIL_PORT=0
|
||||
# Remove the below line if not using TLS - do not set it to False
|
||||
MAIL_USE_TLS=True
|
||||
MAIL_USERNAME=''
|
||||
MAIL_PASSWORD=''
|
||||
MAIL_FROM=''
|
||||
MAIL_ERRORS=False
|
||||
RECAPTCHA3_PUBLIC_KEY=''
|
||||
RECAPTCHA3_PRIVATE_KEY=''
|
||||
MODE='development'
|
||||
|
@ -15,5 +22,7 @@ BOUNCE_HOST=''
|
|||
BOUNCE_USERNAME=''
|
||||
BOUNCE_PASSWORD=''
|
||||
|
||||
FLASK_APP=pyfedi.py
|
||||
SENTRY_DSN=''
|
||||
FLASK_APP = 'pyfedi.py'
|
||||
SENTRY_DSN = ''
|
||||
|
||||
AWS_REGION = ''
|
Loading…
Reference in a new issue