mirror of
https://github.com/QingdaoU/OnlineJudge.git
synced 2024-12-29 16:41:56 +00:00
commit
c41a739e1f
@ -44,3 +44,23 @@ class EditUserSerializer(serializers.Serializer):
|
||||
open_api = serializers.BooleanField()
|
||||
two_factor_auth = serializers.BooleanField()
|
||||
is_disabled = serializers.BooleanField()
|
||||
|
||||
|
||||
class ApplyResetPasswordSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
captcha = serializers.CharField(max_length=4, min_length=4)
|
||||
|
||||
|
||||
class ResetPasswordSerializer(serializers.Serializer):
|
||||
token = serializers.CharField(min_length=1, max_length=40)
|
||||
password = serializers.CharField(min_length=6, max_length=30)
|
||||
captcha = serializers.CharField(max_length=4, min_length=4)
|
||||
|
||||
|
||||
class SSOSerializer(serializers.Serializer):
|
||||
appkey = serializers.CharField(max_length=35)
|
||||
token = serializers.CharField(max_length=40)
|
||||
|
||||
|
||||
class TwoFactorAuthCodeSerializer(serializers.Serializer):
|
||||
code = serializers.IntegerField()
|
||||
|
10
account/tasks.py
Normal file
10
account/tasks.py
Normal file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from celery import shared_task
|
||||
from utils.mail import send_email
|
||||
|
||||
|
||||
@shared_task
|
||||
def _send_email(from_name, to_email, to_name, subject, content):
|
||||
send_email(from_name, to_email, to_name, subject, content)
|
78
account/templates/reset_password_email.html
Normal file
78
account/templates/reset_password_email.html
Normal file
@ -0,0 +1,78 @@
|
||||
<table cellpadding="0" cellspacing="0" align="center" style="text-align:left;font-family:'微软雅黑','黑体',arial;"
|
||||
width="742">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<table cellpadding="0" cellspacing="0"
|
||||
style="text-align:left;border:1px solid #50a5e6;color:#fff;font-size:18px;" width="740">
|
||||
<tbody>
|
||||
<tr height="39" style="background-color:#50a5e6;">
|
||||
<td style="padding-left:15px;font-family:'微软雅黑','黑体',arial;">
|
||||
{{ website_name }} 登录信息找回
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table cellpadding="0" cellspacing="0"
|
||||
style="text-align:left;border:1px solid #f0f0f0;border-top:none;color:#585858;background-color:#fafafa;"
|
||||
width="740">
|
||||
<tbody>
|
||||
<tr height="25">
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
<tr height="40">
|
||||
<td style="padding-left:25px;padding-right:25px;font-size:18px;font-family:'微软雅黑','黑体',arial;">
|
||||
Hello, {{ username }}:
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr height="15">
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr height="30">
|
||||
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:14px;">
|
||||
您刚刚在 {{ website_name }} 申请了找回登录信息服务。
|
||||
</td>
|
||||
</tr>
|
||||
<tr height="30">
|
||||
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:14px;">
|
||||
请在<span style="color:rgb(255,0,0)">30分钟</span>内点击下面链接设置您的新密码:
|
||||
</td>
|
||||
</tr>
|
||||
<tr height="60">
|
||||
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:14px;">
|
||||
<a href="{{ link }}" target="_blank"
|
||||
style="color: rgb(255,255,255);text-decoration: none;display: block;min-height: 39px;width: 158px;line-height: 39px;background-color:rgb(80,165,230);font-size:20px;text-align:center;">重置密码</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr height="10">
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr height="20">
|
||||
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:12px;">
|
||||
如果上面的链接点击无效,请复制以下链接至浏览器的地址栏直接打开。
|
||||
</td>
|
||||
</tr>
|
||||
<tr height="30">
|
||||
<td style="padding-left:55px;padding-right:65px;font-family:'微软雅黑','黑体',arial;">
|
||||
<a href="{{ link }}" target="_blank" style="color:#0c94de;font-size:12px;">
|
||||
{{ link }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr height="20">
|
||||
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:12px;">
|
||||
如果您没有提出过该申请,请忽略此邮件。有可能是其他用户误填了您的邮件地址,我们不会对你的帐户进行任何修改。
|
||||
请不要向他人透露本邮件的内容,否则可能会导致您的账号被盗。
|
||||
</td>
|
||||
</tr>
|
||||
<tr height="20">
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
@ -3,5 +3,5 @@ from django.conf.urls import url
|
||||
from ..views.admin import UserAdminAPI
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^user$", UserAdminAPI.as_view(), name="user_admin_api"),
|
||||
url(r"^user/?$", UserAdminAPI.as_view(), name="user_admin_api"),
|
||||
]
|
||||
|
@ -1,9 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from ..views.oj import UserChangePasswordAPI, UserLoginAPI, UserRegisterAPI
|
||||
from ..views.oj import (UserChangePasswordAPI, UserLoginAPI, UserRegisterAPI,
|
||||
ApplyResetPasswordAPI, ResetPasswordAPI)
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^login$", UserLoginAPI.as_view(), name="user_login_api"),
|
||||
url(r"^register$", UserRegisterAPI.as_view(), name="user_register_api"),
|
||||
url(r"^change_password$", UserChangePasswordAPI.as_view(), name="user_change_password_api")
|
||||
url(r"^login/?$", UserLoginAPI.as_view(), name="user_login_api"),
|
||||
url(r"^register/?$", UserRegisterAPI.as_view(), name="user_register_api"),
|
||||
url(r"^change_password/?$", UserChangePasswordAPI.as_view(), name="user_change_password_api"),
|
||||
url(r"^apply_reset_password/?$", ApplyResetPasswordAPI.as_view(), name="apply_reset_password_api"),
|
||||
url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="apply_reset_password_api")
|
||||
]
|
||||
|
15
account/urls/user.py
Normal file
15
account/urls/user.py
Normal file
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from ..views.user import (UserInfoAPI, UserProfileAPI, AvatarUploadAPI,
|
||||
SSOAPI, TwoFactorAuthAPI)
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^user/?$", UserInfoAPI.as_view(), name="user_info_api"),
|
||||
url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"),
|
||||
url(r"^avatar/upload/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"),
|
||||
url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"),
|
||||
url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api")
|
||||
]
|
@ -1,15 +1,26 @@
|
||||
from django.contrib import auth
|
||||
from django.core.exceptions import MultipleObjectsReturned
|
||||
from django.utils.translation import ugettext as _
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import timedelta
|
||||
from otpauth import OtpAuth
|
||||
|
||||
from django.contrib import auth
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import MultipleObjectsReturned
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.timezone import now
|
||||
|
||||
from conf.models import WebsiteConfig
|
||||
from utils.api import APIView, validate_serializer
|
||||
from utils.captcha import Captcha
|
||||
from utils.shortcuts import rand_str
|
||||
|
||||
from ..decorators import login_required
|
||||
from ..models import User, UserProfile
|
||||
from ..serializers import (UserChangePasswordSerializer, UserLoginSerializer,
|
||||
UserRegisterSerializer)
|
||||
UserRegisterSerializer,
|
||||
ApplyResetPasswordSerializer, ResetPasswordSerializer)
|
||||
from ..tasks import _send_email
|
||||
|
||||
|
||||
class UserLoginAPI(APIView):
|
||||
@ -92,3 +103,55 @@ class UserChangePasswordAPI(APIView):
|
||||
return self.success(_("Succeeded"))
|
||||
else:
|
||||
return self.error(_("Invalid old password"))
|
||||
|
||||
|
||||
class ApplyResetPasswordAPI(APIView):
|
||||
@validate_serializer(ApplyResetPasswordSerializer)
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
captcha = Captcha(request)
|
||||
config = WebsiteConfig.objects.first()
|
||||
if not captcha.check(data["captcha"]):
|
||||
return self.error(_("Invalid captcha"))
|
||||
try:
|
||||
user = User.objects.get(email=data["email"])
|
||||
except User.DoesNotExist:
|
||||
return self.error(_("User does not exist"))
|
||||
if user.reset_password_token_expire_time and 0 < (
|
||||
user.reset_password_token_expire_time - now()).total_seconds() < 20 * 60:
|
||||
return self.error(_("You can only reset password once per 20 minutes"))
|
||||
user.reset_password_token = rand_str()
|
||||
|
||||
user.reset_password_token_expire_time = now() + timedelta(minutes=20)
|
||||
user.save()
|
||||
email_template = open("reset_password_email.html", "w",
|
||||
encoding="utf-8").read()
|
||||
email_template = email_template.replace("{{ username }}", user.username). \
|
||||
replace("{{ website_name }}", settings.WEBSITE_INFO["website_name"]). \
|
||||
replace("{{ link }}", settings.WEBSITE_INFO["url"] + "/reset_password/t/" +
|
||||
user.reset_password_token)
|
||||
_send_email.delay(config.name,
|
||||
user.email,
|
||||
user.username,
|
||||
config.name + " 登录信息找回邮件",
|
||||
email_template)
|
||||
return self.success(_("Succeeded"))
|
||||
|
||||
|
||||
class ResetPasswordAPI(APIView):
|
||||
@validate_serializer(ResetPasswordSerializer)
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
captcha = Captcha(request)
|
||||
if not captcha.check(data["captcha"]):
|
||||
return self.error(_("Invalid captcha"))
|
||||
try:
|
||||
user = User.objects.get(reset_password_token=data["token"])
|
||||
except User.DoesNotExist:
|
||||
return self.error(_("Token dose not exist"))
|
||||
if 0 < (user.reset_password_token_expire_time - now()).total_seconds() < 30 * 60:
|
||||
return self.error(_("Token expired"))
|
||||
user.reset_password_token = None
|
||||
user.set_password(data["password"])
|
||||
user.save()
|
||||
return self.success(_("Succeeded"))
|
||||
|
153
account/views/user.py
Normal file
153
account/views/user.py
Normal file
@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import qrcode
|
||||
|
||||
from io import StringIO
|
||||
from otpauth import OtpAuth
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from conf.models import WebsiteConfig
|
||||
from utils.api import APIView, validate_serializer
|
||||
from utils.shortcuts import rand_str
|
||||
|
||||
from ..decorators import login_required
|
||||
from ..models import User
|
||||
from ..serializers import (EditUserSerializer, UserSerializer,
|
||||
SSOSerializer, TwoFactorAuthCodeSerializer)
|
||||
|
||||
|
||||
class UserInfoAPI(APIView):
|
||||
@login_required
|
||||
def get(self, request):
|
||||
"""
|
||||
Return user info api
|
||||
"""
|
||||
return self.success(UserSerializer(request.user).data)
|
||||
|
||||
|
||||
class UserProfileAPI(APIView):
|
||||
@login_required
|
||||
def get(self, request):
|
||||
"""
|
||||
Return user info api
|
||||
"""
|
||||
return self.success(UserSerializer(request.user).data)
|
||||
|
||||
@validate_serializer(EditUserSerializer)
|
||||
@login_required
|
||||
def put(self, request):
|
||||
data = request.data
|
||||
user_profile = request.user.userprofile
|
||||
if data["avatar"]:
|
||||
user_profile.avatar = data["avatar"]
|
||||
else:
|
||||
user_profile.mood = data["mood"]
|
||||
user_profile.blog = data["blog"]
|
||||
user_profile.school = data["school"]
|
||||
user_profile.student_id = data["student_id"]
|
||||
user_profile.phone_number = data["phone_number"]
|
||||
user_profile.major = data["major"]
|
||||
# Timezone & language 暂时不加
|
||||
user_profile.save()
|
||||
return self.success(_("Succeeded"))
|
||||
|
||||
|
||||
class AvatarUploadAPI(APIView):
|
||||
def post(self, request):
|
||||
if "file" not in request.FILES:
|
||||
return self.error(_("Upload failed"))
|
||||
|
||||
f = request.FILES["file"]
|
||||
if f.size > 1024 * 1024:
|
||||
return self.error(_("Picture too large"))
|
||||
if os.path.splitext(f.name)[-1].lower() not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]:
|
||||
return self.error(_("Unsupported file format"))
|
||||
|
||||
name = "avatar_" + rand_str(5) + os.path.splitext(f.name)[-1]
|
||||
with open(os.path.join(settings.IMAGE_UPLOAD_DIR, name), "wb") as img:
|
||||
for chunk in request.FILES["file"]:
|
||||
img.write(chunk)
|
||||
return self.success({"path": "/static/upload/" + name})
|
||||
|
||||
|
||||
class SSOAPI(APIView):
|
||||
@login_required
|
||||
def get(self, request):
|
||||
callback = request.GET.get("callback", None)
|
||||
if not callback:
|
||||
return self.error(_("Parameter Error"))
|
||||
token = rand_str()
|
||||
request.user.auth_token = token
|
||||
request.user.save()
|
||||
return self.success({"redirect_url": callback + "?token=" + token,
|
||||
"callback": callback})
|
||||
|
||||
@validate_serializer(SSOSerializer)
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
try:
|
||||
User.objects.get(open_api_appkey=data["appkey"])
|
||||
except User.DoesNotExist:
|
||||
return self.error(_("Invalid appkey"))
|
||||
try:
|
||||
user = User.objects.get(auth_token=data["token"])
|
||||
user.auth_token = None
|
||||
user.save()
|
||||
return self.success({"username": user.username,
|
||||
"id": user.id,
|
||||
"admin_type": user.admin_type,
|
||||
"avatar": user.userprofile.avatar})
|
||||
except User.DoesNotExist:
|
||||
return self.error("User does not exist")
|
||||
|
||||
|
||||
class TwoFactorAuthAPI(APIView):
|
||||
@login_required
|
||||
def get(self, request):
|
||||
"""
|
||||
Get QR code
|
||||
"""
|
||||
user = request.user
|
||||
if user.two_factor_auth:
|
||||
return self.error("Already open 2FA")
|
||||
token = rand_str()
|
||||
user.tfa_token = token
|
||||
user.save()
|
||||
|
||||
config = WebsiteConfig.objects.first()
|
||||
image = qrcode.make(OtpAuth(token).to_uri("totp", config.base_url, config.name))
|
||||
buf = StringIO()
|
||||
image.save(buf, "gif")
|
||||
|
||||
return HttpResponse(buf.getvalue(), "image/gif")
|
||||
|
||||
@login_required
|
||||
@validate_serializer(TwoFactorAuthCodeSerializer)
|
||||
def post(self, request):
|
||||
"""
|
||||
Open 2FA
|
||||
"""
|
||||
code = request.data["code"]
|
||||
user = request.user
|
||||
if OtpAuth(user.tfa_token).valid_totp(code):
|
||||
user.two_factor_auth = True
|
||||
user.save()
|
||||
return self.success(_("Succeeded"))
|
||||
else:
|
||||
return self.error(_("Invalid captcha"))
|
||||
|
||||
@login_required
|
||||
@validate_serializer(TwoFactorAuthCodeSerializer)
|
||||
def put(self, request):
|
||||
code = request.data["code"]
|
||||
user = request.user
|
||||
if OtpAuth(user.tfa_token).valid_totp(code):
|
||||
user.two_factor_auth = False
|
||||
user.save()
|
||||
else:
|
||||
return self.error(_("Invalid captcha"))
|
@ -3,5 +3,5 @@ from django.conf.urls import url
|
||||
from ..views import AnnouncementAdminAPI
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^announcement$", AnnouncementAdminAPI.as_view(), name="announcement_admin_api"),
|
||||
url(r"^announcement/?$", AnnouncementAdminAPI.as_view(), name="announcement_admin_api"),
|
||||
]
|
||||
|
@ -3,7 +3,7 @@ from django.conf.urls import url
|
||||
from ..views import SMTPAPI, JudgeServerAPI, WebsiteConfigAPI
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^smtp$", SMTPAPI.as_view(), name="smtp_admin_api"),
|
||||
url(r"^website$", WebsiteConfigAPI.as_view(), name="website_config_api"),
|
||||
url(r"^judge_server", JudgeServerAPI.as_view(), name="judge_server_api")
|
||||
url(r"^smtp/?$", SMTPAPI.as_view(), name="smtp_admin_api"),
|
||||
url(r"^website/?$", WebsiteConfigAPI.as_view(), name="website_config_api"),
|
||||
url(r"^judge_server/?$", JudgeServerAPI.as_view(), name="judge_server_api")
|
||||
]
|
||||
|
@ -3,7 +3,7 @@ from django.conf.urls import url
|
||||
from ..views import JudgeServerHeartbeatAPI, LanguagesAPI, WebsiteConfigAPI
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^website$", WebsiteConfigAPI.as_view(), name="website_info_api"),
|
||||
url(r"^judge_server_heartbeat$", JudgeServerHeartbeatAPI.as_view(), name="judge_server_heartbeat_api"),
|
||||
url(r"^languages$", LanguagesAPI.as_view(), name="language_list_api")
|
||||
url(r"^website/?$", WebsiteConfigAPI.as_view(), name="website_info_api"),
|
||||
url(r"^judge_server_heartbeat/?$", JudgeServerHeartbeatAPI.as_view(), name="judge_server_heartbeat_api"),
|
||||
url(r"^languages/?$", LanguagesAPI.as_view(), name="language_list_api")
|
||||
]
|
||||
|
@ -3,6 +3,6 @@ from django.conf.urls import url
|
||||
from ..views.admin import ContestAnnouncementAPI, ContestAPI
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^contest$", ContestAPI.as_view(), name="contest_api"),
|
||||
url(r"^contest/announcement$", ContestAnnouncementAPI.as_view(), name="contest_announcement_admin_api")
|
||||
url(r"^contest/?$", ContestAPI.as_view(), name="contest_api"),
|
||||
url(r"^contest/announcement/?$", ContestAnnouncementAPI.as_view(), name="contest_announcement_admin_api")
|
||||
]
|
||||
|
@ -3,5 +3,5 @@ from django.conf.urls import url
|
||||
from ..views.oj import ContestAnnouncementListAPI
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^contest$", ContestAnnouncementListAPI.as_view(), name="contest_list_api"),
|
||||
url(r"^contest/?$", ContestAnnouncementListAPI.as_view(), name="contest_list_api"),
|
||||
]
|
||||
|
@ -1,5 +1,5 @@
|
||||
Django<1.10
|
||||
djangorestframework==3.3.3
|
||||
django==1.9.6
|
||||
djangorestframework==3.4.0
|
||||
pillow
|
||||
jsonfield
|
||||
otpauth
|
||||
@ -7,3 +7,6 @@ flake8-quotes
|
||||
pytz
|
||||
coverage
|
||||
python-dateutil
|
||||
celery
|
||||
Envelopes
|
||||
qrcode
|
@ -3,6 +3,7 @@ from django.conf.urls import include, url
|
||||
urlpatterns = [
|
||||
url(r"^api/", include("account.urls.oj")),
|
||||
url(r"^api/admin/", include("account.urls.admin")),
|
||||
url(r"^api/account/", include("account.urls.user")),
|
||||
url(r"^api/admin/", include("announcement.urls.admin")),
|
||||
url(r"^api/", include("conf.urls.oj")),
|
||||
url(r"^api/admin/", include("conf.urls.admin")),
|
||||
|
@ -3,7 +3,7 @@ from django.conf.urls import url
|
||||
from ..views.admin import ProblemAPI, TestCaseUploadAPI, ContestProblemAPI
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^test_case/upload$", TestCaseUploadAPI.as_view(), name="test_case_upload_api"),
|
||||
url(r"^problem$", ProblemAPI.as_view(), name="problem_api"),
|
||||
url(r"^contest/problem$", ContestProblemAPI.as_view(), name="contest_problem_api")
|
||||
url(r"^test_case/upload/?$", TestCaseUploadAPI.as_view(), name="test_case_upload_api"),
|
||||
url(r"^problem/?$", ProblemAPI.as_view(), name="problem_api"),
|
||||
url(r"^contest/problem/?$", ContestProblemAPI.as_view(), name="contest_problem_api")
|
||||
]
|
||||
|
@ -3,5 +3,5 @@ from django.conf.urls import url
|
||||
from ..views.oj import ProblemTagAPI
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^problem/tags$", ProblemTagAPI.as_view(), name="problem_tag_list_api")
|
||||
url(r"^problem/tags/?$", ProblemTagAPI.as_view(), name="problem_tag_list_api")
|
||||
]
|
||||
|
10
requirements.txt
Normal file
10
requirements.txt
Normal file
@ -0,0 +1,10 @@
|
||||
django==1.9.6
|
||||
djangorestframework==3.4.0
|
||||
otpauth
|
||||
pillow
|
||||
python-dateutil
|
||||
celery
|
||||
Envelopes
|
||||
pytz
|
||||
jsonfield
|
||||
qrcode
|
21
utils/mail.py
Normal file
21
utils/mail.py
Normal file
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from envelopes import Envelope
|
||||
|
||||
from conf.models import SMTPConfig
|
||||
|
||||
|
||||
def send_email(from_name, to_email, to_name, subject, content):
|
||||
smtp = SMTPConfig.objects.first()
|
||||
if not smtp:
|
||||
return
|
||||
envlope = Envelope(from_addr=(smtp.email, from_name),
|
||||
to_addr=(to_email, to_name),
|
||||
subject=subject,
|
||||
html_body=content)
|
||||
envlope.send(smtp.server,
|
||||
login=smtp.email,
|
||||
password=smtp.password,
|
||||
port=smtp.port,
|
||||
tls=smtp.tls)
|
Loading…
Reference in New Issue
Block a user