diff --git a/apps/common/tasks.py b/apps/common/tasks.py index d5c0cb60921e..5bffe07f447e 100644 --- a/apps/common/tasks.py +++ b/apps/common/tasks.py @@ -2,7 +2,7 @@ from celery import shared_task from django.conf import settings -from django.core.mail import send_mail, EmailMultiAlternatives +from django.core.mail import send_mail, EmailMultiAlternatives, get_connection from django.utils.translation import gettext_lazy as _ import jms_storage @@ -11,6 +11,16 @@ logger = get_logger(__file__) +def get_email_connection(**kwargs): + email_backend_map = { + 'smtp': 'django.core.mail.backends.smtp.EmailBackend', + 'exchange': 'jumpserver.rewriting.exchange.EmailBackend' + } + return get_connection( + backend=email_backend_map.get(settings.EMAIL_PROTOCOL), **kwargs + ) + + def task_activity_callback(self, subject, message, recipient_list, *args, **kwargs): from users.models import User email_list = recipient_list @@ -40,7 +50,7 @@ def send_mail_async(*args, **kwargs): args = tuple(args) try: - return send_mail(*args, **kwargs) + return send_mail(connection=get_email_connection(), *args, **kwargs) except Exception as e: logger.error("Sending mail error: {}".format(e)) @@ -55,7 +65,8 @@ def send_mail_attachment_async(subject, message, recipient_list, attachment_list subject=subject, body=message, from_email=from_email, - to=recipient_list + to=recipient_list, + connection=get_email_connection(), ) for attachment in attachment_list: email.attach_file(attachment) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index f5f5d5d0c7e8..18e570271b4d 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -453,6 +453,7 @@ class Config(dict): 'CUSTOM_SMS_REQUEST_METHOD': 'get', # Email + 'EMAIL_PROTOCOL': 'smtp', 'EMAIL_CUSTOM_USER_CREATED_SUBJECT': _('Create account successfully'), 'EMAIL_CUSTOM_USER_CREATED_HONORIFIC': _('Hello'), 'EMAIL_CUSTOM_USER_CREATED_BODY': _('Your account has been created successfully'), diff --git a/apps/jumpserver/rewriting/exchange.py b/apps/jumpserver/rewriting/exchange.py new file mode 100644 index 000000000000..709e5558fdfc --- /dev/null +++ b/apps/jumpserver/rewriting/exchange.py @@ -0,0 +1,104 @@ +import urllib3 + +from urllib3.exceptions import InsecureRequestWarning + +from django.core.mail.backends.base import BaseEmailBackend +from django.core.mail.message import sanitize_address +from django.conf import settings +from exchangelib import Account, Credentials, Configuration, DELEGATE +from exchangelib import Mailbox, Message, HTMLBody, FileAttachment +from exchangelib import BaseProtocol, NoVerifyHTTPAdapter +from exchangelib.errors import TransportError + + +urllib3.disable_warnings(InsecureRequestWarning) +BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter + + +class EmailBackend(BaseEmailBackend): + def __init__( + self, + service_endpoint=None, + username=None, + password=None, + fail_silently=False, + **kwargs, + ): + super().__init__(fail_silently=fail_silently) + self.service_endpoint = service_endpoint or settings.EMAIL_HOST + self.username = settings.EMAIL_HOST_USER if username is None else username + self.password = settings.EMAIL_HOST_PASSWORD if password is None else password + self._connection = None + + def open(self): + if self._connection: + return False + + try: + config = Configuration( + service_endpoint=self.service_endpoint, credentials=Credentials( + username=self.username, password=self.password + ) + ) + self._connection = Account(self.username, config=config, access_type=DELEGATE) + return True + except TransportError: + if not self.fail_silently: + raise + + def close(self): + self._connection = None + + def send_messages(self, email_messages): + if not email_messages: + return 0 + + new_conn_created = self.open() + if not self._connection or new_conn_created is None: + return 0 + num_sent = 0 + for message in email_messages: + sent = self._send(message) + if sent: + num_sent += 1 + if new_conn_created: + self.close() + return num_sent + + def _send(self, email_message): + if not email_message.recipients(): + return False + + encoding = settings.DEFAULT_CHARSET + from_email = sanitize_address(email_message.from_email, encoding) + recipients = [ + Mailbox(email_address=sanitize_address(addr, encoding)) for addr in email_message.recipients() + ] + try: + message_body = email_message.body + alternatives = email_message.alternatives or [] + attachments = [] + for attachment in email_message.attachments or []: + name, content, mimetype = attachment + if isinstance(content, str): + content = content.encode(encoding) + attachments.append( + FileAttachment(name=name, content=content, content_type=mimetype) + ) + for alternative in alternatives: + if alternative[1] == 'text/html': + message_body = HTMLBody(alternative[0]) + break + + email_message = Message( + account=self._connection, subject=email_message.subject, + body=message_body, to_recipients=recipients, sender=from_email, + attachments=[] + ) + email_message.attach(attachments) + email_message.send_and_save() + except Exception as error: + if not self.fail_silently: + raise error + return False + return True diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index aebb737418c1..4fdba8b971e4 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -331,6 +331,7 @@ def parse_sentinels_host(sentinels_host): FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'), ] # Email config +EMAIL_PROTOCOL = CONFIG.EMAIL_PROTOCOL EMAIL_HOST = CONFIG.EMAIL_HOST EMAIL_PORT = CONFIG.EMAIL_PORT EMAIL_HOST_USER = CONFIG.EMAIL_HOST_USER diff --git a/apps/settings/api/email.py b/apps/settings/api/email.py index 1e8ef2286500..3790752d5ac6 100644 --- a/apps/settings/api/email.py +++ b/apps/settings/api/email.py @@ -4,11 +4,12 @@ from smtplib import SMTPSenderRefused from django.conf import settings -from django.core.mail import send_mail, get_connection +from django.core.mail import send_mail from django.utils.translation import gettext_lazy as _ from rest_framework.views import Response, APIView from common.utils import get_logger +from common.tasks import get_email_connection as get_connection from .. import serializers logger = get_logger(__file__) diff --git a/apps/settings/serializers/msg.py b/apps/settings/serializers/msg.py index 07e8e7205b2f..ace4ab42c1f4 100644 --- a/apps/settings/serializers/msg.py +++ b/apps/settings/serializers/msg.py @@ -1,11 +1,12 @@ # coding: utf-8 # - +from django.db import models from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from common.serializers.fields import EncryptedField + __all__ = [ 'MailTestSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer', 'SMSBackendSerializer', @@ -18,14 +19,20 @@ class MailTestSerializer(serializers.Serializer): class EmailSettingSerializer(serializers.Serializer): - # encrypt_fields 现在使用 write_only 来判断了 PREFIX_TITLE = _('Email') - EMAIL_HOST = serializers.CharField(max_length=1024, required=True, label=_("SMTP host")) - EMAIL_PORT = serializers.CharField(max_length=5, required=True, label=_("SMTP port")) - EMAIL_HOST_USER = serializers.CharField(max_length=128, required=True, label=_("SMTP account")) + class EmailProtocol(models.TextChoices): + smtp = 'smtp', _('SMTP') + exchange = 'exchange', _('EXCHANGE') + + EMAIL_PROTOCOL = serializers.ChoiceField( + choices=EmailProtocol.choices, label=_("Protocol"), default=EmailProtocol.smtp + ) + EMAIL_HOST = serializers.CharField(max_length=1024, required=True, label=_("Host")) + EMAIL_PORT = serializers.CharField(max_length=5, required=True, label=_("Port")) + EMAIL_HOST_USER = serializers.CharField(max_length=128, required=True, label=_("Account")) EMAIL_HOST_PASSWORD = EncryptedField( - max_length=1024, required=False, label=_("SMTP password"), + max_length=1024, required=False, label=_("Password"), help_text=_("Tips: Some provider use token except password") ) EMAIL_FROM = serializers.CharField(