9c8d0d109a
Сотрудник может подключить несколько IMAP-ящиков и переключаться между ними. Дислокация снова показывает всю сеть, а не только назначенные аптеки. Co-authored-by: Cursor <cursoragent@cursor.com>
716 lines
26 KiB
Python
716 lines
26 KiB
Python
"""Портал корпоративной почты (IMAP/SMTP) — подключение ящика самим сотрудником."""
|
|
import imaplib
|
|
import logging
|
|
import re
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.http import Http404, HttpResponse, HttpResponseForbidden
|
|
from django.shortcuts import render
|
|
from django.utils import timezone
|
|
from django.views.decorators.http import require_POST
|
|
|
|
from apps.mail_connector.access import (
|
|
can_use_portal_mail,
|
|
get_user_mailbox,
|
|
list_user_mailboxes,
|
|
mailbox_address,
|
|
set_active_mailbox,
|
|
suggested_mailbox_email,
|
|
)
|
|
from apps.mail_connector.autoconfig import AutoconfigResult, list_mail_candidates, source_label
|
|
from apps.mail_connector.imap_client import ImapMailClient, MailConnectionError
|
|
from apps.mail_connector.mail_auth import clear_mail_password, get_mail_password, is_mail_connected
|
|
from apps.mail_connector.mail_errors import humanize_mail_error
|
|
from apps.mail_connector.mail_settings import MailConfigError, MailSettings, get_mail_settings, resolve_mail_settings
|
|
from apps.mail_connector.models import UserMailbox
|
|
from apps.mail_connector.smtp_client import SmtpSendError, send_message
|
|
from core.portal_access import section_required
|
|
from core.utils.htmx import render_page
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_EMAIL_ADDR_RE = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]+$')
|
|
|
|
MAIL_ATTACH_MAX_FILES = 5
|
|
MAIL_ATTACH_MAX_BYTES = 10 * 1024 * 1024 # 10 MB per file
|
|
MAIL_ATTACH_MAX_TOTAL = 15 * 1024 * 1024 # 15 MB total (Django upload limit)
|
|
|
|
|
|
def _collect_attachments(request) -> tuple[list[tuple[str, bytes, str]], list[str]]:
|
|
"""Зчитати вкладення з multipart-форми. Повертає (attachments, errors)."""
|
|
files = request.FILES.getlist('attachments')
|
|
if not files:
|
|
return [], []
|
|
|
|
errors: list[str] = []
|
|
if len(files) > MAIL_ATTACH_MAX_FILES:
|
|
errors.append(f'Не більше {MAIL_ATTACH_MAX_FILES} файлів.')
|
|
return [], errors
|
|
|
|
attachments: list[tuple[str, bytes, str]] = []
|
|
total = 0
|
|
for f in files:
|
|
name = (getattr(f, 'name', None) or 'file').strip() or 'file'
|
|
size = getattr(f, 'size', 0) or 0
|
|
if size <= 0:
|
|
continue
|
|
if size > MAIL_ATTACH_MAX_BYTES:
|
|
errors.append(f'«{name}» більший за 10 МБ.')
|
|
continue
|
|
total += size
|
|
if total > MAIL_ATTACH_MAX_TOTAL:
|
|
errors.append('Сумарний розмір вкладень більший за 15 МБ.')
|
|
break
|
|
content = f.read()
|
|
ctype = getattr(f, 'content_type', None) or 'application/octet-stream'
|
|
attachments.append((name, content, ctype))
|
|
return attachments, errors
|
|
|
|
|
|
def _email_guard(view_func):
|
|
@login_required
|
|
@section_required('email')
|
|
def _wrapped(request, *args, **kwargs):
|
|
if not can_use_portal_mail(request.user):
|
|
return HttpResponseForbidden('Нет доступа к почте.')
|
|
return view_func(request, *args, **kwargs)
|
|
return _wrapped
|
|
|
|
|
|
def _base_context(request, **extra):
|
|
try:
|
|
defaults = get_mail_settings()
|
|
mail_configured = True
|
|
mail_error = None
|
|
except MailConfigError as e:
|
|
defaults = None
|
|
mail_configured = False
|
|
mail_error = str(e)
|
|
|
|
mailboxes = list(list_user_mailboxes(request.user, connected_only=True))
|
|
return {
|
|
'mailbox': mailbox_address(request.user),
|
|
'mailboxes': mailboxes,
|
|
'suggested_email': suggested_mailbox_email(request.user),
|
|
'mail_connected': is_mail_connected(request),
|
|
'mail_configured': mail_configured,
|
|
'mail_error': mail_error,
|
|
'mail_defaults': defaults,
|
|
'adding_mailbox': False,
|
|
'active_folder': request.GET.get('folder', 'inbox'),
|
|
'folder_choices': [
|
|
('inbox', 'Вхідні'),
|
|
('sent', 'Відправлені'),
|
|
('drafts', 'Чернетки'),
|
|
('trash', 'Кошик'),
|
|
('starred', 'Важливі'),
|
|
],
|
|
**extra,
|
|
}
|
|
|
|
|
|
def _with_imap(request, fn):
|
|
"""Выполнить fn(client) или вернуть (error_key, error_msg)."""
|
|
box = get_user_mailbox(request.user)
|
|
address = mailbox_address(request.user)
|
|
password = get_mail_password(request)
|
|
if not address or not box:
|
|
return None, 'not_connected', None
|
|
if not password:
|
|
return None, 'not_connected', None
|
|
try:
|
|
settings = resolve_mail_settings(box)
|
|
except MailConfigError as e:
|
|
return None, 'not_configured', str(e)
|
|
try:
|
|
with ImapMailClient(settings, address, password) as client:
|
|
return fn(client), None, None
|
|
except (MailConnectionError, imaplib.IMAP4.error, OSError) as e:
|
|
logger.warning('IMAP %s: %s', address, e)
|
|
return None, 'connection_error', str(e)
|
|
|
|
|
|
MAIL_PAGE_SIZE = 40
|
|
|
|
|
|
@_email_guard
|
|
def email_index(request):
|
|
ctx = _base_context(request)
|
|
if not ctx['mail_configured']:
|
|
return render_page(request, 'email.html', ctx)
|
|
|
|
adding = request.GET.get('add') == '1'
|
|
if adding and ctx['mailboxes']:
|
|
ctx['mail_state'] = 'connect'
|
|
ctx['adding_mailbox'] = True
|
|
ctx['suggested_email'] = ''
|
|
return render_page(request, 'email.html', ctx)
|
|
|
|
if not ctx['mail_connected']:
|
|
ctx['mail_state'] = 'connect'
|
|
return render_page(request, 'email.html', ctx)
|
|
|
|
folder = ctx['active_folder']
|
|
starred = folder == 'starred'
|
|
search = request.GET.get('q', '').strip()
|
|
read_uid = request.GET.get('uid') if request.GET.get('view') == 'read' else None
|
|
|
|
def load(client):
|
|
messages, has_more = client.list_messages(
|
|
'inbox' if starred else folder,
|
|
limit=MAIL_PAGE_SIZE,
|
|
offset=0,
|
|
search_text=search,
|
|
starred_only=starred,
|
|
)
|
|
unread = client.unread_count('inbox')
|
|
message = None
|
|
if read_uid:
|
|
try:
|
|
message = client.fetch_message(read_uid, 'inbox' if starred else folder)
|
|
except MailConnectionError as e:
|
|
logger.warning('fetch_message uid=%s: %s', read_uid, e)
|
|
return messages, has_more, unread, None, str(e)
|
|
return messages, has_more, unread, message, None
|
|
|
|
result, err_key, err_msg = _with_imap(request, load)
|
|
if err_key:
|
|
ctx['mail_state'] = err_key
|
|
ctx['mail_error'] = err_msg
|
|
if err_key in ('not_connected', 'connection_error'):
|
|
if err_key == 'connection_error':
|
|
failed = get_user_mailbox(request.user)
|
|
host = (failed.imap_host if failed else '') or ''
|
|
port = failed.imap_port if failed else None
|
|
clear_mail_password(request)
|
|
if is_mail_connected(request):
|
|
# Есть другой ящик — переключаемся на него
|
|
response = HttpResponse('')
|
|
response['HX-Redirect'] = '/email/'
|
|
return response
|
|
ctx['connect_error'] = humanize_mail_error(
|
|
Exception(err_msg or 'connection failed'),
|
|
host=host,
|
|
port=port,
|
|
)
|
|
ctx['mail_state'] = 'connect'
|
|
ctx['mail_connected'] = False
|
|
ctx['mailbox'] = None
|
|
ctx['mailboxes'] = list(list_user_mailboxes(request.user, connected_only=True))
|
|
ctx['suggested_email'] = suggested_mailbox_email(request.user)
|
|
return render_page(request, 'email.html', ctx)
|
|
|
|
messages, has_more, unread, message, read_err = result
|
|
ctx.update({
|
|
'mail_state': 'ready',
|
|
'messages': messages,
|
|
'unread_count': unread,
|
|
'search_q': search,
|
|
'view': request.GET.get('view', 'list'),
|
|
'compose': request.GET.get('view') == 'compose',
|
|
'mail_has_more': has_more,
|
|
'mail_next_offset': MAIL_PAGE_SIZE if has_more else 0,
|
|
'mail_page_size': MAIL_PAGE_SIZE,
|
|
})
|
|
|
|
if message:
|
|
ctx['message'] = message
|
|
elif read_uid:
|
|
ctx['read_error'] = read_err or 'Не вдалося відкрити лист'
|
|
|
|
return render_page(request, 'email.html', ctx)
|
|
|
|
|
|
@_email_guard
|
|
def email_list_more(request):
|
|
"""HTMX: следующая страница списка писем."""
|
|
folder = request.GET.get('folder', 'inbox')
|
|
starred = folder == 'starred'
|
|
search = request.GET.get('q', '').strip()
|
|
try:
|
|
offset = max(0, int(request.GET.get('offset', 0)))
|
|
except (TypeError, ValueError):
|
|
offset = 0
|
|
|
|
def load(client):
|
|
return client.list_messages(
|
|
'inbox' if starred else folder,
|
|
limit=MAIL_PAGE_SIZE,
|
|
offset=offset,
|
|
search_text=search,
|
|
starred_only=starred,
|
|
)
|
|
|
|
result, err_key, err_msg = _with_imap(request, load)
|
|
if err_key:
|
|
return render(request, 'partials/email-list-more.html', {
|
|
'messages': [],
|
|
'mail_has_more': False,
|
|
'active_folder': folder,
|
|
'search_q': search,
|
|
'message': None,
|
|
'load_error': err_msg or 'Не вдалося завантажити',
|
|
})
|
|
|
|
messages, has_more = result or ([], False)
|
|
return render(request, 'partials/email-list-more.html', {
|
|
'messages': messages,
|
|
'mail_has_more': has_more,
|
|
'mail_next_offset': offset + MAIL_PAGE_SIZE if has_more else offset,
|
|
'active_folder': folder,
|
|
'search_q': search,
|
|
'message': None,
|
|
})
|
|
|
|
|
|
def _render_connect_panel(request, context):
|
|
ctx = {
|
|
'mail_defaults': _mail_defaults_or_none(),
|
|
'form_imap_host': '',
|
|
'form_imap_port': '',
|
|
'form_smtp_host': '',
|
|
'form_smtp_port': '',
|
|
'form_use_ssl': True,
|
|
'autoconfig_open': bool(context.get('connect_error')),
|
|
'autoconfig_message': '',
|
|
**context,
|
|
}
|
|
return render(request, 'partials/email-connect-panel.html', ctx)
|
|
|
|
|
|
def _mail_defaults_or_none():
|
|
try:
|
|
return get_mail_settings()
|
|
except MailConfigError:
|
|
return None
|
|
|
|
|
|
def _parse_optional_port(raw):
|
|
raw = (raw or '').strip()
|
|
if not raw:
|
|
return None
|
|
try:
|
|
port = int(raw)
|
|
except ValueError:
|
|
return None
|
|
if port < 1 or port > 65535:
|
|
return None
|
|
return port
|
|
|
|
|
|
def _candidate_settings(cand: AutoconfigResult) -> MailSettings:
|
|
return MailSettings(
|
|
imap_host=cand.imap_host,
|
|
imap_port=cand.imap_port,
|
|
smtp_host=cand.smtp_host,
|
|
smtp_port=cand.smtp_port,
|
|
use_ssl=cand.use_ssl,
|
|
verify_ssl=False,
|
|
)
|
|
|
|
|
|
def _build_connect_candidates(email, imap_host, imap_port, smtp_host, smtp_port, use_ssl):
|
|
"""Ручной ввод первым, затем автокандидаты (как Thunderbird пробует по очереди)."""
|
|
candidates: list[AutoconfigResult] = []
|
|
if imap_host or smtp_host:
|
|
try:
|
|
base = get_mail_settings()
|
|
except MailConfigError:
|
|
base = None
|
|
candidates.append(AutoconfigResult(
|
|
imap_host=imap_host or (base.imap_host if base else ''),
|
|
imap_port=imap_port or (base.imap_port if base else 993),
|
|
smtp_host=smtp_host or imap_host or (base.smtp_host if base else ''),
|
|
smtp_port=smtp_port or (base.smtp_port if base else 465),
|
|
use_ssl=use_ssl,
|
|
source='manual',
|
|
provider_id='manual',
|
|
score=200,
|
|
))
|
|
for cand in list_mail_candidates(email, include_portal=True):
|
|
if not cand.imap_host:
|
|
continue
|
|
candidates.append(cand)
|
|
# дедуп
|
|
seen = set()
|
|
out = []
|
|
for c in candidates:
|
|
k = c.key()
|
|
if k in seen or not c.imap_host:
|
|
continue
|
|
seen.add(k)
|
|
out.append(c)
|
|
return out
|
|
|
|
|
|
@_email_guard
|
|
@require_POST
|
|
def email_autoconfig(request):
|
|
"""HTMX: подобрать IMAP/SMTP по email и вернуть блок расширенных полей."""
|
|
email = (request.POST.get('email') or '').strip()
|
|
defaults = _mail_defaults_or_none()
|
|
ctx = {
|
|
'mail_defaults': defaults,
|
|
'form_imap_host': '',
|
|
'form_imap_port': '',
|
|
'form_smtp_host': '',
|
|
'form_smtp_port': '',
|
|
'form_use_ssl': True,
|
|
'autoconfig_open': False,
|
|
'autoconfig_message': '',
|
|
}
|
|
|
|
if not email or not _EMAIL_ADDR_RE.match(email):
|
|
ctx['autoconfig_message'] = 'Вкажіть коректний email для автовизначення сервера.'
|
|
return render(request, 'partials/email-autoconfig-fields.html', ctx)
|
|
|
|
candidates = list_mail_candidates(email, include_portal=True)
|
|
# для UI предпочитаем не-portal, если есть probe/ispdb
|
|
preferred = next(
|
|
(c for c in candidates if c.source not in ('portal', 'guess')),
|
|
candidates[0] if candidates else None,
|
|
)
|
|
if preferred is None and defaults:
|
|
ctx['autoconfig_message'] = (
|
|
f'Автоконфіг не знайдено — буде використано сервер порталу '
|
|
f'({defaults.imap_host}:{defaults.imap_port}).'
|
|
)
|
|
elif preferred is None:
|
|
ctx['autoconfig_open'] = True
|
|
ctx['autoconfig_message'] = 'Автоконфіг не знайдено. Вкажіть сервер вручну.'
|
|
else:
|
|
ctx.update(preferred.as_form_dict())
|
|
ctx['autoconfig_open'] = preferred.source != 'portal'
|
|
extras = [c for c in candidates if c.key() != preferred.key()][:3]
|
|
extra_txt = ''
|
|
if extras:
|
|
extra_txt = ' · також: ' + ', '.join(f'{c.imap_host}:{c.imap_port}' for c in extras)
|
|
ctx['autoconfig_message'] = (
|
|
f'Знайдено ({source_label(preferred.source)}): '
|
|
f'IMAP {preferred.imap_host}:{preferred.imap_port}, '
|
|
f'SMTP {preferred.smtp_host}:{preferred.smtp_port}'
|
|
f'{extra_txt}'
|
|
)
|
|
|
|
return render(request, 'partials/email-autoconfig-fields.html', ctx)
|
|
|
|
|
|
@_email_guard
|
|
@require_POST
|
|
def email_connect(request):
|
|
email = (request.POST.get('email') or '').strip()
|
|
password = request.POST.get('password', '')
|
|
imap_host = (request.POST.get('imap_host') or '').strip()
|
|
smtp_host = (request.POST.get('smtp_host') or '').strip()
|
|
imap_port = _parse_optional_port(request.POST.get('imap_port'))
|
|
smtp_port = _parse_optional_port(request.POST.get('smtp_port'))
|
|
use_ssl = (request.POST.get('use_ssl') or '1') != '0'
|
|
|
|
form_ctx = {
|
|
'suggested_email': email or suggested_mailbox_email(request.user),
|
|
'form_imap_host': imap_host,
|
|
'form_smtp_host': smtp_host,
|
|
'form_imap_port': request.POST.get('imap_port') or '',
|
|
'form_smtp_port': request.POST.get('smtp_port') or '',
|
|
'form_use_ssl': use_ssl,
|
|
'mail_defaults': _mail_defaults_or_none(),
|
|
'autoconfig_open': True,
|
|
'adding_mailbox': list_user_mailboxes(request.user, connected_only=True).exists(),
|
|
}
|
|
|
|
def _fail(msg, detail=''):
|
|
return _render_connect_panel(request, {
|
|
**form_ctx,
|
|
'connect_error': msg,
|
|
'connect_error_detail': detail,
|
|
})
|
|
|
|
if not email or not _EMAIL_ADDR_RE.match(email):
|
|
return _fail('Вкажіть коректну адресу поштової скриньки.')
|
|
if not password:
|
|
return _fail('Введіть пароль поштової скриньки.')
|
|
|
|
candidates = _build_connect_candidates(
|
|
email, imap_host, imap_port, smtp_host, smtp_port, use_ssl,
|
|
)
|
|
if not candidates:
|
|
return _fail(
|
|
'Не вдалося визначити поштовий сервер.',
|
|
'Вкажіть IMAP-хост у розширених налаштуваннях або увімкніть коннектор пошти в порталі.',
|
|
)
|
|
|
|
last_error: BaseException | None = None
|
|
last_cand: AutoconfigResult | None = None
|
|
tried: list[str] = []
|
|
|
|
for cand in candidates:
|
|
settings = _candidate_settings(cand)
|
|
tried.append(f'{cand.imap_host}:{cand.imap_port}')
|
|
try:
|
|
with ImapMailClient(settings, email, password):
|
|
pass
|
|
except MailConnectionError as e:
|
|
last_error, last_cand = e, cand
|
|
logger.info('IMAP probe fail %s@%s:%s — %s', email, cand.imap_host, cand.imap_port, e)
|
|
continue
|
|
except Exception as e:
|
|
last_error, last_cand = e, cand
|
|
logger.warning('IMAP probe unexpected %s@%s:%s — %s', email, cand.imap_host, cand.imap_port, e)
|
|
continue
|
|
|
|
# успех — не затираем другие ящики; уникальность (employee, email)
|
|
box, _ = UserMailbox.objects.update_or_create(
|
|
employee=request.user,
|
|
email=email,
|
|
defaults={
|
|
'password': password,
|
|
'imap_host': cand.imap_host,
|
|
'imap_port': cand.imap_port,
|
|
'smtp_host': cand.smtp_host,
|
|
'smtp_port': cand.smtp_port,
|
|
'use_ssl': cand.use_ssl,
|
|
'verify_ssl': False,
|
|
'is_connected': True,
|
|
'last_ok_at': timezone.now(),
|
|
},
|
|
)
|
|
set_active_mailbox(request.user, box)
|
|
response = _render_connect_panel(request, {
|
|
**form_ctx,
|
|
'suggested_email': email,
|
|
'form_imap_host': cand.imap_host,
|
|
'form_imap_port': cand.imap_port,
|
|
'form_smtp_host': cand.smtp_host,
|
|
'form_smtp_port': cand.smtp_port,
|
|
'form_use_ssl': cand.use_ssl,
|
|
'connect_success': True,
|
|
})
|
|
response['HX-Redirect'] = '/email/'
|
|
return response
|
|
|
|
host = last_cand.imap_host if last_cand else ''
|
|
port = last_cand.imap_port if last_cand else None
|
|
msg = humanize_mail_error(last_error or Exception('unknown'), host=host, port=port)
|
|
detail = ''
|
|
if len(tried) > 1:
|
|
detail = 'Спробовано: ' + ', '.join(tried[:6])
|
|
if len(tried) > 6:
|
|
detail += '…'
|
|
# показать в форме последний/лучший хост
|
|
if last_cand:
|
|
form_ctx.update({
|
|
'form_imap_host': last_cand.imap_host,
|
|
'form_imap_port': last_cand.imap_port,
|
|
'form_smtp_host': last_cand.smtp_host,
|
|
'form_smtp_port': last_cand.smtp_port,
|
|
'form_use_ssl': last_cand.use_ssl,
|
|
'autoconfig_open': True,
|
|
'autoconfig_message': 'Перевірте хост/пароль або оберіть інший сервер нижче.',
|
|
})
|
|
return _fail(msg, detail)
|
|
|
|
|
|
@_email_guard
|
|
@require_POST
|
|
def email_disconnect(request):
|
|
clear_mail_password(request)
|
|
response = _render_connect_panel(request, {
|
|
'suggested_email': suggested_mailbox_email(request.user),
|
|
'connect_success': False,
|
|
'adding_mailbox': False,
|
|
'mailboxes': list(list_user_mailboxes(request.user, connected_only=True)),
|
|
})
|
|
response['HX-Redirect'] = '/email/'
|
|
return response
|
|
|
|
|
|
@_email_guard
|
|
@require_POST
|
|
def email_switch(request):
|
|
"""Переключить активный ящик: mailbox_id = uuid или pk."""
|
|
raw = (request.POST.get('mailbox_id') or '').strip()
|
|
box = None
|
|
if raw:
|
|
qs = request.user.mailboxes.filter(is_connected=True).exclude(password='')
|
|
box = qs.filter(uuid=raw).first()
|
|
if box is None and raw.isdigit():
|
|
box = qs.filter(pk=int(raw)).first()
|
|
if box is None:
|
|
return HttpResponseForbidden('Скриньку не знайдено.')
|
|
set_active_mailbox(request.user, box)
|
|
response = HttpResponse('')
|
|
response['HX-Redirect'] = '/email/'
|
|
return response
|
|
|
|
|
|
@_email_guard
|
|
@require_POST
|
|
def email_send(request):
|
|
to_raw = request.POST.get('to', '').strip()
|
|
subject = request.POST.get('subject', '').strip()
|
|
body = request.POST.get('body', '').strip()
|
|
cc_raw = request.POST.get('cc', '').strip()
|
|
bcc_raw = request.POST.get('bcc', '').strip()
|
|
|
|
to_addrs = [a.strip() for a in to_raw.replace(';', ',').split(',') if a.strip()]
|
|
cc_addrs = [a.strip() for a in cc_raw.replace(';', ',').split(',') if a.strip()]
|
|
bcc_addrs = [a.strip() for a in bcc_raw.replace(';', ',').split(',') if a.strip()]
|
|
attachments, attach_errors = _collect_attachments(request)
|
|
|
|
errors = []
|
|
if not to_addrs:
|
|
errors.append('Вкажіть одержувача.')
|
|
if not subject:
|
|
errors.append('Вкажіть тему.')
|
|
if not body:
|
|
errors.append('Вкажіть текст листа.')
|
|
for addr in to_addrs + cc_addrs + bcc_addrs:
|
|
if not _EMAIL_ADDR_RE.match(addr):
|
|
errors.append(f'Некоректний email: {addr}')
|
|
errors.extend(attach_errors)
|
|
|
|
box = get_user_mailbox(request.user)
|
|
address = mailbox_address(request.user)
|
|
password = get_mail_password(request)
|
|
reply_uid = (request.POST.get('reply_uid') or '').strip()
|
|
reply_folder = request.POST.get('reply_folder') or 'inbox'
|
|
ctx = _base_context(
|
|
request,
|
|
compose=not bool(reply_uid),
|
|
form_to=to_raw,
|
|
form_cc=cc_raw,
|
|
form_bcc=bcc_raw,
|
|
form_subject=subject,
|
|
form_body=body,
|
|
active_folder=reply_folder if reply_uid else request.GET.get('folder', 'inbox'),
|
|
)
|
|
|
|
def _reload_reply(extra):
|
|
ctx['mail_state'] = 'ready'
|
|
ctx['view'] = 'read'
|
|
ctx['compose'] = False
|
|
ctx.update(extra)
|
|
starred = reply_folder == 'starred'
|
|
|
|
def load_one(client):
|
|
msg = client.fetch_message(reply_uid, 'inbox' if starred else reply_folder)
|
|
unread = client.unread_count('inbox')
|
|
return msg, unread
|
|
|
|
loaded, _err_key, err_msg = _with_imap(request, load_one)
|
|
if loaded:
|
|
ctx['message'], ctx['unread_count'] = loaded
|
|
else:
|
|
ctx['read_error'] = err_msg or 'Не вдалося відкрити лист'
|
|
return render_page(request, 'email.html', ctx)
|
|
|
|
def _reload_compose(extra):
|
|
ctx['mail_state'] = 'ready'
|
|
ctx['view'] = 'compose'
|
|
ctx['compose'] = True
|
|
ctx.update(extra)
|
|
folder = ctx.get('active_folder') or 'inbox'
|
|
starred = folder == 'starred'
|
|
|
|
def load_list(client):
|
|
messages, has_more = client.list_messages(
|
|
'inbox' if starred else folder,
|
|
limit=MAIL_PAGE_SIZE,
|
|
offset=0,
|
|
starred_only=starred,
|
|
)
|
|
unread = client.unread_count('inbox')
|
|
return messages, has_more, unread
|
|
|
|
loaded, _err_key, _err_msg = _with_imap(request, load_list)
|
|
if loaded:
|
|
messages, has_more, unread = loaded
|
|
ctx['messages'] = messages
|
|
ctx['unread_count'] = unread
|
|
ctx['mail_has_more'] = has_more
|
|
ctx['mail_next_offset'] = MAIL_PAGE_SIZE if has_more else 0
|
|
return render_page(request, 'email.html', ctx)
|
|
|
|
if not password or not box:
|
|
if reply_uid:
|
|
return _reload_reply({'send_errors': ['Підключіть поштову скриньку.']})
|
|
ctx['send_errors'] = ['Підключіть поштову скриньку.']
|
|
ctx['mail_state'] = 'connect'
|
|
return render_page(request, 'email.html', ctx)
|
|
|
|
if errors:
|
|
if reply_uid:
|
|
return _reload_reply({'send_errors': errors})
|
|
return _reload_compose({'send_errors': errors})
|
|
|
|
try:
|
|
settings = resolve_mail_settings(box)
|
|
send_message(
|
|
settings,
|
|
username=address,
|
|
password=password,
|
|
sender=address,
|
|
to_addrs=to_addrs,
|
|
subject=subject,
|
|
body=body,
|
|
cc_addrs=cc_addrs,
|
|
bcc_addrs=bcc_addrs,
|
|
attachments=attachments,
|
|
)
|
|
except MailConfigError as e:
|
|
if reply_uid:
|
|
return _reload_reply({'send_errors': [str(e)]})
|
|
return _reload_compose({'send_errors': [str(e)]})
|
|
except SmtpSendError as e:
|
|
if reply_uid:
|
|
return _reload_reply({'send_errors': [f'Помилка відправки: {e}']})
|
|
return _reload_compose({'send_errors': [f'Помилка відправки: {e}']})
|
|
|
|
if reply_uid:
|
|
return _reload_reply({'send_success': True, 'form_body': ''})
|
|
|
|
ctx['mail_state'] = 'ready'
|
|
ctx['send_success'] = True
|
|
ctx['view'] = 'list'
|
|
ctx['active_folder'] = 'sent'
|
|
ctx['compose'] = False
|
|
|
|
def load(client):
|
|
messages, has_more = client.list_messages('sent', limit=MAIL_PAGE_SIZE, offset=0)
|
|
unread = client.unread_count('inbox')
|
|
return messages, has_more, unread
|
|
|
|
result, err_key, err_msg = _with_imap(request, load)
|
|
if result:
|
|
messages, has_more, unread = result
|
|
ctx['messages'] = messages
|
|
ctx['unread_count'] = unread
|
|
ctx['mail_has_more'] = has_more
|
|
ctx['mail_next_offset'] = MAIL_PAGE_SIZE if has_more else 0
|
|
return render_page(request, 'email.html', ctx)
|
|
|
|
|
|
@_email_guard
|
|
def email_attachment(request):
|
|
from urllib.parse import quote
|
|
|
|
uid = (request.GET.get('uid') or '').strip()
|
|
folder = request.GET.get('folder', 'inbox')
|
|
if folder == 'starred':
|
|
folder = 'inbox'
|
|
try:
|
|
index = int(request.GET.get('i', ''))
|
|
except (TypeError, ValueError):
|
|
raise Http404('Файл не знайдено')
|
|
|
|
def load(client):
|
|
return client.fetch_attachment(uid, folder, index)
|
|
|
|
result, _err_key, _err_msg = _with_imap(request, load)
|
|
if not result:
|
|
raise Http404('Файл не знайдено')
|
|
name, payload, ctype = result
|
|
resp = HttpResponse(payload, content_type=ctype or 'application/octet-stream')
|
|
resp['Content-Disposition'] = f"attachment; filename*=UTF-8''{quote(name)}"
|
|
return resp
|