django通過封裝python的smtplib實(shí)現(xiàn)發(fā)送郵件功能访惜。django 1.11官網(wǎng)翻譯內(nèi)容見:http://www.reibang.com/p/c02aac458a71。
下面的內(nèi)容結(jié)合django by example CH2 第一節(jié)的內(nèi)容完成鸦列,翻譯了第一節(jié)的內(nèi)容复凳,使用python2.7+Django1.11測試,修正了書中代碼的錯誤渗常,添加了注意事項(xiàng)席揽,并寫出了測試結(jié)果。
通過e-mail分享文章
首先矩欠,我們允許用戶通過發(fā)送郵件的方式分享文章财剖。想一下我們?nèi)绾瓮ㄟ^上一章學(xué)到views、URLs和templates實(shí)現(xiàn)這個功能癌淮。如果要允許用戶通過e-mail發(fā)送郵件躺坟,我們需要:
- 為用戶創(chuàng)建一個form來填寫它們的名字、e-mail该默,接收文章的e-mail和評論(可選)瞳氓;
- 在views.py文件中創(chuàng)建一個視圖來處理post的數(shù)據(jù)并發(fā)送e-mail;
- 在urls.py文件中為新建立的視圖添加URL栓袖。
- 創(chuàng)建一個模板來展示表單匣摘。
使用Django創(chuàng)建form
form即為表單,下面表述中form與表單意義相同裹刮。
我們從創(chuàng)建分享文章的form開始音榜。Django內(nèi)置form框架幫助我們非常方便的創(chuàng)建form。form礦建允許我們定義form的字段捧弃、指定它們展示的方式赠叼、輸入數(shù)據(jù)的驗(yàn)證方式擦囊。Django form礦建還提供靈活的方法來渲染form和處理數(shù)據(jù)。
Django提供兩個基準(zhǔn)類來創(chuàng)建forms:
Form:幫助我們創(chuàng)建標(biāo)準(zhǔn)forms嘴办;
ModelForm:幫助我們創(chuàng)建增加或者修改模型實(shí)例的forms瞬场。
首先,在blog應(yīng)用的根目錄新建一個名為forms.py的文件涧郊,并添加以下代碼:
from django import forms
class EmailPostForm(forms.Form):
name = forms.CharField(max_length=25)
email = forms.EmailField()
to = forms.EmailField()
comments = forms.CharField(required=False, widget=forms.Textarea)
這是你的第一個django表單(form)贯被。我們來看一下通過集成Form類創(chuàng)建的form。我們使用不同的字段對輸入進(jìn)行驗(yàn)證妆艘。
注意:
Forms可以放在Django項(xiàng)目的任何位置彤灶,為了方便起見,我們將其放在每個項(xiàng)目的forms.py文件中批旺。
name字段是一個CharField幌陕。這種類型的字段渲染一個<input type="text">
的HTML元素。每一個字段都對應(yīng)一個小組件汽煮,這個小組件決定HTML如何展示該字段搏熄。默認(rèn)的組件可以通過設(shè)置widget屬性進(jìn)行覆蓋。在comments字段中逗物,我們使用Textarea組件將其展示為一個<textarea>
HTML元素來代替默認(rèn)的<input>
元素搬卒。
字段驗(yàn)證還依賴字段類型。例如翎卓,email和to字段為EmailField,兩個字段都需要有效地e-mail地址摆寄,否則字段驗(yàn)證將引發(fā)forms.ValidationError異常并且form無法通過驗(yàn)證失暴。form驗(yàn)證還會考慮其他參數(shù):我們定義了一個最大長度為25的name字段并將comment字段設(shè)置為required=False。form驗(yàn)證時會將這些都考慮在內(nèi)微饥。這個form中使用的字段類型只是django表單字段的一小部分逗扒,所有的表單字段可以參考https://docs.djangoproject.com/en/1.11/ref/forms/fields/。
在視圖中處理表單
我們需要創(chuàng)建了一個新的視圖來處理表單并在它成功提交時發(fā)送e-mail欠橘。編輯blog應(yīng)用的views.py寫入以下代碼:
from .forms import EmailPostForm
def post_share(request, post_id):
# Retrieve post by id
post = get_object_or_404(Post, id=post_id, status='published')
if request.method == 'POST':
# Form was submitted
form = EmailPostForm(request.POST)
if form.is_valid():
# Form fields passed validation
cd = form.cleaned_data
# ... send email
else:
form = EmailPostForm()
return render(request, 'blog/post/share.html', {'post': post, 'form': form})
該表單將實(shí)現(xiàn)以下功能:
我們定義了post_share視圖矩肩,該視圖輸入request對象和post_id作為參數(shù)。
我們使用get_object_or_404()通過id獲取文章并且要求文章狀態(tài)為published肃续。
展示初始表單和處理提交的數(shù)據(jù)使用相同的視圖黍檩。我們使用request方法對其進(jìn)行區(qū)分,如果request方法為GET始锚,我們將展示一個空的表單刽酱;如果request方法為POST,那么表單將被提交并且需要處理瞧捌。因此我們使用request.method="POST"來區(qū)分這兩種情況棵里。
下面是展示和處理表單的過程:
當(dāng)使用GET請求視圖時润文,我們創(chuàng)建一個新的form類在模板中展示空的表單:
form=EmailPostForm()
用戶填寫表單并通過POST提交,然后殿怜,我們在POST部分使用提交的數(shù)據(jù)創(chuàng)建了一個表單類視圖:
if form.is_valid():
# Form fields passed validation
cd = form.cleaned_data
# ... send email
然后典蝌,我們使用is_valid方法對提交的數(shù)據(jù)進(jìn)行驗(yàn)證,這個方法對表單中的數(shù)據(jù)進(jìn)行驗(yàn)證头谜,如果數(shù)據(jù)均為有效數(shù)據(jù)赠法,則會返回Ture,否則會返回False乔夯。如果驗(yàn)證為False我們可以通過訪問form.errors看到錯誤列表:
如果表單沒有通過驗(yàn)證砖织,我們將使用提交的數(shù)據(jù)再次渲染表單,并且在模板中顯示驗(yàn)證錯誤末荐。
-
如果表單通過驗(yàn)證侧纯,我們通過form.cleaned_data獲取數(shù)據(jù),這個屬性為表單字段名和值的屬性甲脏。
注意:
如果表單字段沒有驗(yàn)證眶熬,cleaned_data將值包含通過驗(yàn)證的字段。
現(xiàn)在我們需要學(xué)習(xí)如何使用Django發(fā)送郵件了块请。
使用Django發(fā)送郵件
使用Django發(fā)送郵件非常簡單娜氏。首先,我們需要一個本地SMTP服務(wù)器或者在項(xiàng)目setting.py中添加以下設(shè)置來配置一個外部SMTP服務(wù)器:
EMAIL_HOST: SMTP服務(wù)器主機(jī)墩新。默認(rèn)為localhost贸弥;
EMAIL_PORT: SMTP服務(wù)器端口。默認(rèn)為25海渊;
EMAIL_HOST_USER: the SMTP 服務(wù)器的用戶名绵疲;
EMAIL_HOST_PASSWORD: SMTP 服務(wù)器的密碼;
EMAIL_USE_TLS: 是否使用TLS安全連接臣疑;
EMAIL_USE_SSL: 是否使用隱式TLS安全連接盔憨。
如果沒有本地SMTP服務(wù)器,可以使用e-mail提供者的SMTP服務(wù)器讯沈。下面的簡單配置是使用hotmail賬戶通過hotmail服務(wù)器發(fā)送e-mail的配置(https://outlook.live.com/owa/?path=/options/popandimap):
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'your_account@gmail.com'
EMAIL_HOST_PASSWORD = 'your_password'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
注意:
配置中郁岩,EMAIL_USE_TLS和EMAIL_USE_SSL都默認(rèn)設(shè)置為False,需要配置其中一個為True缺狠,但是不能兩個都設(shè)置為True问慎。一般端口587對應(yīng)TLS,端口465對應(yīng)SSL(加強(qiáng)TSL)儒老。
在teminal的項(xiàng)目根目錄輸入命令:python manage.py shell打開Python shell并發(fā)送郵件:
from django.core.mail import send_mail
>>> send_mail('Django mail', 'This e-mail was sent with Django.', 'your_account@gmail.com', ['your_account@gmail.com'], fail_silently=False)
send_mail()輸入主題蝴乔、消息、發(fā)送者驮樊、接受者列表作為參數(shù)薇正,通過設(shè)置fail_silently=False可以在郵件沒有正確發(fā)送的時候引發(fā)異常片酝。如果輸出為1,那么郵件就正常發(fā)送了挖腰。如果采用setting.py中設(shè)置google web服務(wù)器發(fā)送郵件雕沿,需要正常訪問以下網(wǎng)址:https://www.google.com/settings/security/lesssecureapps。
發(fā)送郵件測試
國內(nèi)無法訪問https://www.google.com/settings/security/lesssecureapps猴仑。因此审轮,測試了hotmail郵箱和163郵箱:
測試環(huán)境:python2.7,Django1.11
hotmail郵箱
下面的簡單配置是使用hotmail賬戶通過hotmail服務(wù)器發(fā)送e-mail的配置(https://outlook.live.com/owa/?path=/options/popandimap):
EMAIL_HOST = 'smtp-mail.outlook.com'
EMAIL_HOST_USER = 'your_account@hotmail.com'
EMAIL_HOST_PASSWORD = 'your_password'
EMAIL_PORT= 587
EMAIL_USE_TLS = True
在teminal的項(xiàng)目根目錄輸入命令:python manage.py shell打開Python shell并發(fā)送郵件:
from django.core.mail import send_mail
>>> send_mail('Django mail', 'This e-mail was sent with Django.', 'your_account@hotmail.com', ['your_account@163.com'], fail_silently=False)
如果send_mail第三個參數(shù)
與EMAIL_HOST_USER一致辽俗,郵件正常發(fā)送疾渣。但是如果接收郵件只有阿里云企業(yè)郵箱,該郵箱沒有返回崖飘,需要在程序中增加時間限制榴捡,否則郵件發(fā)送正常但是程序會長時間等待接收反饋信息而無法執(zhí)行其他命令。
如果send_mail第三個參數(shù)與EMAIL_HOST_USER不一致朱浴,會引發(fā)SMTPDataError 異常吊圾,無法發(fā)送郵件。
163郵箱
EMAIL_HOST = 'smtp.163.com'
EMAIL_HOST_USER = 'your_account@163.com'
EMAIL_HOST_PASSWORD = 'your_auth_code' #郵箱的授權(quán)碼而非密碼
EMAIL_PORT = 465
EMAIL_USE_SSL = True
注意翰蠢,這里的password不是郵箱密碼项乒,而是在[郵箱]-[設(shè)置]-[POP3/SMTP/IMAP]中設(shè)置的授權(quán)碼。
在teminal的項(xiàng)目根目錄輸入命令:python manage.py shell打開Python shell并發(fā)送郵件:
from django.core.mail import send_mail
>>> send_mail('Django mail', 'This e-mail was sent with Django.', 'your_account@hotmail.com', ['your_account@163.com'], fail_silently=False)
如果send_mail第三個參數(shù)與EMAIL_HOST_USER一致梁沧,引發(fā)SMTPDataError: (554, 'DT:SPM 163 smtp11,D8CowADnvQK5UwFa6C2ZAw--.46120S2 1510036409,please see http://mail.163.com/help/help_spam_16.htm?ip=120.194.143.53&hostid=smtp11&time=1510036409')異常檀何,
http://mail.163.com/help/help_spam_16.htm?ip=120.194.143.53&hostid=smtp11&time=1510036409的對應(yīng)內(nèi)容為該郵件被視為垃圾郵件,更改主題與內(nèi)容后仍無效趁尼。需要進(jìn)一步了解163判斷垃圾郵件的依據(jù)埃碱。
如果send_mail第三個參數(shù)為hotmail郵箱,郵件無法發(fā)送酥泞,引發(fā)SMTPSenderRefused: (553, 'Mail from must equal authorized user')異常。即發(fā)送信息郵箱必須與授權(quán)郵箱一致啃憎。
發(fā)送郵件測試總結(jié)
測試環(huán)境:python2.7+Django1.11芝囤。
盡量使用hotmail郵件作為settings中的郵箱;
send_mail中的發(fā)送郵箱最好與settings中的授權(quán)郵箱一致辛萍。
現(xiàn)在悯姊,將發(fā)送郵件功能添加到視圖中,將post_share視圖更改為:
from .forms import EmailPostForm
from django.core.mail import send_mail
def post_share(request, post_id):
# Retrieve post by id
post = get_object_or_404(Post, id=post_id, status='published')
sent = False
if request.method == 'POST':
# Form was submitted
form = EmailPostForm(request.POST)
if form.is_valid():
# Form fields passed validation
cd = form.cleaned_data
post_url = request.build_absolute_uri(post.get_absolute_url())
subject = '{}({})recommends you read "{}"'.format(cd['name'],
cd['email'],
post.title)
message = 'read"{}" at {} \n\n\'s comments:{}'.format(post.title,
post_url,
cd['name'],
cd[
'comments'])
send_mail(subject, message, cd['email'], [cd['to']])
sent = True
# ... send email
else:
form = EmailPostForm()
return render(request, 'blog/post/share.html',
{'post': post, 'form': form, 'sent': sent})
這里贩毕,我們定義了一個變量sent悯许,當(dāng)郵件發(fā)送成功時,sent設(shè)為True辉阶。后續(xù)我們會在模板中使用該變量先壕,當(dāng)表單正確提交且郵件發(fā)送成功時顯示成功信息瘩扼。這里使用get_absolute_url()方法獲取在郵件中用到的文章的鏈接,我們將該函數(shù)作為request.build_absolute_uri()的輸入來構(gòu)建包含http的完整URL垃僚。我們使用驗(yàn)證的表單的cleaned data作為郵件的主題和內(nèi)容集绰,然后將郵件發(fā)送到表單中to一欄中添加的地址中。
現(xiàn)在視圖完成了谆棺,我們?yōu)槠涮砑覷RL栽燕,打開blog應(yīng)用下的urls.py文件,添加以下內(nèi)容:
urlpatterns = [
# ...
url(r'^(?P<post_id>\d+)/share/$', views.post_share,
name='post_share'),
]
在模板中渲染表單
創(chuàng)建完表單改淑,完成視圖并添加URL后碍岔,我們還需要為這個視圖添加模板。在blog/templates/blog/post目錄下創(chuàng)建名為share.html的文件朵夏,添加以下內(nèi)容:
{% extends "blog/base.html" %}
{% block title %}Share a post{% endblock %}
{% block content %}
{% if sent %}
<h1>E-mail successfully sent</h1>
<p>
"{{ post.title }}" was successfully sent to {{ form.to }}.
</p>
{% else %}
<h1>Share "{{ post.title }}" by e-mail</h1>
<form action="." method="post">
{{ form.as_p }}
{% csrf_token %}
<input type="submit" value="Send e-mail">
</form>
{% endif %}
{% endblock %}
這是展示表單的模板蔼啦,當(dāng)發(fā)送郵件成功時該模板則會顯示成功信息。我們創(chuàng)建了通過POST提交的表單:
<form action="." method="post">
然后侍郭,我們包含了form實(shí)例询吴,我們通過as_p方法告訴Django將字段渲染為HTML的p元素(我們還可以通過as_table方法將字段渲染為table或者通過as_ul方法將字段渲染為ul)。如果我們要渲染每個字段亮元,我們可以對每個字段進(jìn)行迭代:
{% for field in form %}
<div>
{{ field.errors }}
{{ field.label_tag }}{{ field.field }}
</div>
{% endfor %}
模板標(biāo)簽中的{% csrf_token %}模板標(biāo)簽引入一個自動生成避免CSRF襲擊所用token的隱藏字段猛计。關(guān)于CSRF可以從以下網(wǎng)站獲取更多信息:“https://en.wikipedia.org/wiki/Cross-site_request_forgery。該字段將會自動生成一個隱藏輸入:
<input type='hidden' name='csrfmiddlewaretoken' value='4WZ53yXqRomGrnH1xFeaXlVGeqPQzgJdGAE3D9ZoWpY9qknlUFLNyRMgFqATIRea' />
注意:
默認(rèn)爆捞,Django為所有POST請求檢查CSRFtoken奉瘤。所以我們需要通過POST提交的表單添加csrf_token。
編輯blog/post/detail.html模板并在{{ post.body|linebreaks }}后添加分享鏈接:
<p>
<a href="{% url "blog:post_share" post.id %}">
Share this post
</a>
</p>
我們通過Django的url模板標(biāo)簽動態(tài)生成URL煮甥。我們使用了命名空間為blog名稱為post_share的URL盗温,我們將post.id作為參數(shù)傳入絕對URL中。
現(xiàn)在成肘,在teminal中項(xiàng)目根目錄下運(yùn)行:
python manage.py runserver
在瀏覽器中打開http://127.0.0.1:8000/blog/卖局,點(diǎn)擊任意文章標(biāo)題到文章詳細(xì)內(nèi)容頁面,在正文后面双霍,你可以看到以下鏈接:
點(diǎn)擊Share this post砚偶,我們將跳到分享郵件分享頁面:
表單的css位于static/css/blog.css文件匯總。當(dāng)我們點(diǎn)擊send e-mail按鈕時洒闸,表單將被提交并驗(yàn)證染坯。如果所有字段都通過驗(yàn)證,且郵件正常發(fā)送丘逸,我們將得到以下頁面:
如果輸入包含無效數(shù)據(jù)单鹿,根據(jù)瀏覽器的不同,可能點(diǎn)擊send e-mail顯示錯誤提示從而無法跳轉(zhuǎn)深纲,也可能跳轉(zhuǎn)后顯示錯誤提示仲锄,重新填寫表單(通過驗(yàn)證的字段無需重復(fù)填寫)劲妙。