[译] 使用 Django 构建一个简单的邮件服务

当咱们建立一个 web 应用时,常常会有发送邮件的须要。不管是用户注册,仍是用户忘记密码,又或者是用户下单后进行付款确认,都须要发送不一样的邮件。因此发送邮件的需求其实是很是重要的,并且若是不在一开始就构建结构清晰的邮件服务,到后面可能会是一团糟。html

这篇文章不只会教你如何定义一个能轻松切换发送平台的电子邮件服务,而且还能够对实际的邮件内容进行定制。前端

本文中使用 Django 来作出示例,但我但愿你能够将其中主要的思路应用到其余你可能正在使用的语言或者框架中。python

让咱们开始吧!android

基本的邮件发送

假设咱们但愿在用户注册到咱们的 web 应用后向他发送邮件。咱们能够参照 Django 文档,向验证经过并建立成功的用户发送邮件。具体实现以下:ios

import logging

from rest_framework.views import APIView
from django.http import JsonResponse
from django.core.mail import send_mail

from users.models import User

logger = logging.getLogger('django')


class RegisterView(APIView):

    def post(self, request):
        # Run validations
        if not request.data:
            return JsonResponse({'errors': 'User data must be provided'}, status=400)
        if User.objects.filter(email=request.data['email']).exists():
            return JsonResponse({'errors': 'Email already in use'}, status=400)
        try:
            # Create new user
            user = User.objects.create_user(email=request.data['email'].lower())
            user.set_password(request.data['password'])
            user.save()
            
            # Send welcome email
            send_mail(
                subject='Welcome!',
                message='Hey there! Welcome to our platform.',
                html_message='<p><strong>Het there!</strong> Welcome to our platform.</p>'
                from_email='from@example.com',
                recipient_list=[user.email],
                fail_silently=False,
            )
            
            return JsonResponse({'status': 'ok'})
        except Exception as e:
            logger.error('Error at %s', 'register view', exc_info=e)
            return JsonResponse({'errors': 'Wrong data provided'}, status=400)
复制代码

固然正如文档中所说的那样,你也必须提早设定好一些重要的配置项,好比 EMAIL_HOST 和 EMAIL_PORT。git

很好!如今咱们已经发送了欢迎邮件!github

建立一个 mailer 类

正如我以前所说,在咱们的应用中不一样的模块可能都须要发送邮件,因此最好有一个电子邮件服务或者 mailer 类来处理全部的邮件请求。这样子会让改动和迭代邮件的需求变得更简单,由于咱们再也不须要每次都去翻遍所有代码。web

import logging

from django.conf import settings
from django.core.mail import send_mail

from users.models import User

logger = logging.getLogger('django')


class BaseMailer():
    def __init__(self, to_email, subject, message, html_message):
        self.to_email = to_email
        self.subject = subject
        self.message = message
        self.html_message = html_message

    def send_email(self):
        send_mail(
            subject=self.subject,
            message=self.message,
            html_message=self.html_message,
            from_email='from@example.com',
            recipient_list=[self.to_email],
            fail_silently=False,
        )
复制代码

让咱们来看看通过此次改变后,注册服务的视图层是什么样子:django

import logging

from rest_framework.views import APIView
from django.http import JsonResponse
from django.core.mail import send_mail

from users.models import User
from users.mailers import BasicMailer

logger = logging.getLogger('django')


class RegisterView(APIView):

    def post(self, request):
        # Run validations
        if not request.data:
            return JsonResponse({'errors': 'User data must be provided'}, status=400)
        if User.objects.filter(email=request.data['email']).exists():
            return JsonResponse({'errors': 'Email already in use'}, status=400)
        try:
            # Create new user
            user = User.objects.create_user(email=request.data['email'].lower())
            user.set_password(request.data['password'])
            user.save()
            
            # Send welcome email
            BasicMailer(to_email=user.email, 
                        subject='Welcome!', 
                        message='Hey there! Welcome to our platform.', 
                        html_message='<p><strong>Het there!</strong> Welcome to our platform.</p>').send_email()
            
            return JsonResponse({'status': 'ok'})
        except Exception as e:
            logger.error('Error at %s', 'register view', exc_info=e)
            return JsonResponse({'errors': 'Wrong data provided'}, status=400)
复制代码

mailer 子类

如今咱们已经把全部的“邮件代码”移动到一个单独的地方,能够把它利用起来啦!这时候就能继续建立特定的 mailer 类,并让它们知道每次被调用时该发送什么内容。如今让咱们建立一个 mailer 类用来在每次用户注册时进行调用,另外一个 mailer 类用来发送订单的确认信息。后端

import logging

from django.conf import settings
from django.core.mail import send_mail

from users.models import User

logger = logging.getLogger('django')


class BaseMailer():
    def __init__(self, to_email, subject, message, html_message):
        self.to_email = to_email
        self.subject = subject
        self.message = message
        self.html_message = html_message

    def send_email(self):
        send_mail(
            subject=self.subject,
            message=self.message,
            html_message=self.html_message,
            from_email='from@example.com',
            recipient_list=[self.to_email],
            fail_silently=False,
        )
        
        
class RegisterMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email,  
                         subject='Welcome!', 
                         message='Hey there! Welcome to our platform.', 
                         html_message='<p><strong>Het there!</strong> Welcome to our platform.</p>')
        

class NewOrderMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email,  
                         subject='New Order', 
                         message='You have just created a new order', 
                         html_message='<p>You have just created a new order.</p>')
复制代码

这代表在不一样的场景下集成邮件服务是很是简单的。你只须要构造一个基础的 mail 类来进行实现,而后在子类中设置具体内容。

由于不用实现全部邮件相关的代码,因此如今咱们注册服务的视图层看起来更加简明:

import logging

from rest_framework.views import APIView
from django.http import JsonResponse
from django.core.mail import send_mail

from users.models import User
from users.mailers import RegisterMailer

logger = logging.getLogger('django')


class RegisterView(APIView):

    def post(self, request):
        # Run validations
        if not request.data:
            return JsonResponse({'errors': 'User data must be provided'}, status=400)
        if User.objects.filter(email=request.data['email']).exists():
            return JsonResponse({'errors': 'Email already in use'}, status=400)
        try:
            # Create new user
            user = User.objects.create_user(email=request.data['email'].lower())
            user.set_password(request.data['password'])
            user.save()
            
            # Send welcome email
            RegisterMailer(to_email=user.email).send_email()
            
            return JsonResponse({'status': 'ok'})
        except Exception as e:
            logger.error('Error at %s', 'register view', exc_info=e)
            return JsonResponse({'errors': 'Wrong data provided'}, status=400)
复制代码

使用 Sendgrid

假设咱们必须使用正式的 python 库 将咱们的邮件服务后端迁移 Sendgrid (一个用于交易和营销邮件的客户通讯平台)。咱们将不能再使用 Django 的 send_email 方法,并且咱们还不得不使用新库的语法。嗯,但咱们仍是很幸运地!由于咱们已经将全部与邮件管理相关的代码都放到了一个单独的地方,因此咱们能够很轻松的作出此次改动 😉

import logging

from django.conf import settings
from sendgrid import SendGridAPIClient, Email, Personalization
from sendgrid.helpers.mail import Mail

from users.models import User

logger = logging.getLogger('django')


class BaseMailer():
    def __init__(self, email, subject, template_id):
        self.mail = Mail()
        self.subject = subject
        self.template_id = template_id

    def create_email(self):
        self.mail.from_email = Email(settings.FROM_EMAIL)
        self.mail.subject = self.subject
        self.mail.template_id = self.template_id
        personalization = Personalization()
        personalization.add_to(Email(self.user.email))
        self.mail.add_personalization(personalization)

    def send_email(self):
        self.create_email()
        try:
            sg = SendGridAPIClient(settings.SENDGRID_API_KEY)
            sg.send(self.mail)
        except Exception as e:
            logger.error('Error at %s', 'mailer', exc_info=e)
            

class RegisterMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email, subject='Welcome!', template_id=1234)

        
class NewOrderMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email, subject='New Order', template_id=5678)
复制代码

请注意你必须在配置文件中设置 Sendgrid 的 api 密钥,以及指定须要被使用的模板的 ID,Sendrid 会直接从本身的页面中根据这个 ID 来加载指定的 html 邮件模版。

太好了!这并不困难,并且咱们不用去修改发送邮件的每一行代码。

如今让咱们的步子再迈大一点。

根据域信息定制邮件内容

固然咱们发送邮件的时候,有时候可能也会使用一些域信息来填充模板。好比说,若是在欢迎邮件里面能有新用户的名字,那确定会显得更友好。Sendgrid 容许你在邮件模板中定义变量,这些变量将替换为从咱们这里接收的实际信息。因此如今让咱们来添加这部分数据吧!

import logging

from django.conf import settings
from sendgrid import SendGridAPIClient, Email, Personalization
from sendgrid.helpers.mail import Mail

from users.models import User

logger = logging.getLogger('django')


class BaseMailer():
    def __init__(self, email, subject, template_id):
        self.mail = Mail()
        self.user = User.objects.get(email=email)
        self.subject = subject
        self.template_id = template_id
        self.substitutions = {
            'user_name': self.user.first_name,
            'user_surname': self.user.last_name
        }


    def create_email(self):
        self.mail.from_email = Email(settings.FROM_EMAIL)
        self.mail.subject = self.subject
        self.mail.template_id = self.template_id
        personalization = Personalization()
        personalization.add_to(Email(self.user.email))
        personalization.dynamic_template_data = self.substitutions
        self.mail.add_personalization(personalization)

    def send_email(self):
        self.create_email()
        try:
            sg = SendGridAPIClient(settings.SENDGRID_API_KEY)
            sg.send(self.mail)
        except Exception as e:
            logger.error('Error at %s', 'mailer', exc_info=e)
            

class RegisterMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email, subject='Welcome!', template_id=1234)

        
class NewOrderMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email, subject='New Order', template_id=5678)
复制代码

这里我看到的惟一一个问题是替换方案不那么灵活。极可能会发生的状况是,咱们传递的数据多是用户根据请求的上下文访问不到的。好比说,新的订单编号、重置密码的连接等等。这些变量参数可能不少,把它们做为命名参数传递可能会让代码变得比较脏乱。咱们但愿的是一个基于关键字,而且长度可变的参数列表,通常来讲会被定义为 **kwargs,但在此咱们命名它为 **substitutions,让这个表达会更形象:

import logging

from django.conf import settings
from sendgrid import SendGridAPIClient, Email, Personalization
from sendgrid.helpers.mail import Mail

from users.models import User

logger = logging.getLogger('django')


class BaseMailer():
    def __init__(self, email, subject, template_id, **substitutions):
        self.mail = Mail()
        self.user = User.objects.get(email=email)
        self.subject = subject
        self.template_id = template_id
        self.substitutions = {
            'user_name': self.user.first_name,
            'user_surname': self.user.last_name
        }
        
        for key in substitutions:
            self.substitutions.update({key: substitutions[key]})

    def create_email(self):
        self.mail.from_email = Email(settings.FROM_EMAIL)
        self.mail.subject = self.subject
        self.mail.template_id = self.template_id
        personalization = Personalization()
        personalization.add_to(Email(self.user.email))
        personalization.dynamic_template_data = self.substitutions
        self.mail.add_personalization(personalization)

    def send_email(self):
        self.create_email()
        try:
            sg = SendGridAPIClient(settings.SENDGRID_API_KEY)
            sg.send(self.mail)
        except Exception as e:
            logger.error('Error at %s', 'mailer', exc_info=e)
            

class RegisterMailer(BaseMailer):
    def __init__(self, to_email, **substitutions):
        super().__init__(to_email, subject='Welcome!', template_id=1234, **substitutions)

        
class NewOrderMailer(BaseMailer):
    def __init__(self, to_email):
        super().__init__(to_email, subject='New Order', template_id=5678, **substitutions)
复制代码

若是但愿将额外信息传递给 mailer 类,就须要按以下编码:

NewOrderMailer(user.email, order_id=instance.id).send_email()
PasswordResetMailer(user.email, key=password_token.key).send_email()
复制代码

总结

咱们已经建立了一个灵活的 mailer 类,它将全部与电子邮件相关的代码封装在一个单独的地方,使代码维护变得更容易,而且还接收可变的上下文参数来填充电子邮件内容!一会儿设计这整个方案确定会很困难,可是咱们一步一步的去实现就会简单得多,而且会在这个过程当中受益良多。我鼓励你基于这个设计,继续开发邮件附件的功能!

很是感谢你阅读这篇文章,我但愿它能对你的项目有所帮助。也请关注我即将发布的帖子,祝你人生幸运,编码愉快。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索