ad8730c752
Co-authored-by: Cursor <cursoragent@cursor.com>
305 lines
18 KiB
HTML
305 lines
18 KiB
HTML
{# Rainloop-style webmail layout #}
|
|
{% load i18n %}
|
|
{% if not mail_configured %}
|
|
<div class="page-content">
|
|
<div class="mail-notice">{{ mail_error }}</div>
|
|
</div>
|
|
|
|
{% elif mail_state == 'connect' %}
|
|
<div class="page-content">
|
|
{% include "partials/email-connect-panel.html" %}
|
|
</div>
|
|
|
|
{% else %}
|
|
<div class="mail-app">
|
|
<header class="mail-toolbar">
|
|
<div class="mail-toolbar-left">
|
|
{% if mailboxes|length > 1 %}
|
|
<form class="mail-toolbar-switch"
|
|
hx-post="/email/switch/"
|
|
hx-target="#main-content"
|
|
hx-indicator="#loader"
|
|
hx-trigger="change">
|
|
{% csrf_token %}
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 7L2 7"/></svg>
|
|
<select name="mailbox_id" class="mail-toolbar-select" title="{% trans "Почтовый ящик" %}" aria-label="{% trans "Почтовый ящик" %}">
|
|
{% for box in mailboxes %}
|
|
<option value="{{ box.uuid }}"{% if box.is_active %} selected{% endif %}>{{ box.email }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</form>
|
|
{% else %}
|
|
<span class="mail-toolbar-brand" title="{{ mailbox }}">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 7L2 7"/></svg>
|
|
<span class="mail-toolbar-email">{{ mailbox }}</span>
|
|
</span>
|
|
{% endif %}
|
|
<a href="/email/?add=1"
|
|
hx-get="/email/?add=1"
|
|
hx-target="#main-content" hx-push-url="/email/?add=1" hx-indicator="#loader"
|
|
class="mail-btn mail-btn-ghost" title="{% trans "Подключить ещё один ящик" %}">{% trans "Добавить" %}</a>
|
|
<a href="/email/?view=compose&folder={{ active_folder }}"
|
|
hx-get="/email/?view=compose&folder={{ active_folder }}"
|
|
hx-target="#main-content" hx-push-url="/email/?view=compose&folder={{ active_folder }}" hx-indicator="#loader"
|
|
class="mail-btn mail-btn-compose">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
|
|
{% trans "Написать" %}
|
|
</a>
|
|
</div>
|
|
<form class="mail-toolbar-search" method="get" action="/email/"
|
|
hx-get="/email/"
|
|
hx-target="#main-content"
|
|
hx-push-url="true"
|
|
hx-indicator="#loader"
|
|
hx-trigger="submit, keyup changed delay:450ms from:input[name='q'], search from:input[name='q']">
|
|
<input type="hidden" name="folder" value="{{ active_folder }}">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
|
<input type="search" name="q" value="{{ search_q|default:'' }}"
|
|
placeholder="{% trans "Тема, email, домен…" %}"
|
|
autocomplete="off"
|
|
enterkeyhint="search">
|
|
</form>
|
|
<div class="mail-toolbar-right">
|
|
<a href="/email/?folder={{ active_folder }}{% if search_q %}&q={{ search_q|urlencode }}{% endif %}"
|
|
hx-get="/email/?folder={{ active_folder }}{% if search_q %}&q={{ search_q|urlencode }}{% endif %}"
|
|
hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
|
|
class="mail-btn mail-btn-ghost" title="{% trans "Обновить" %}">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-9-9c2.5 0 4.8 1 6.5 2.7L21 8"/><path d="M21 3v5h-5"/></svg>
|
|
</a>
|
|
<form hx-post="/email/disconnect/" hx-target="#main-content" hx-indicator="#loader" style="margin:0;">
|
|
{% csrf_token %}
|
|
<button type="submit" class="mail-btn mail-btn-ghost" title="{% trans "Выйти из ящика" %}">{% trans "Выйти" %}</button>
|
|
</form>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="mail-layout{% if compose %} mail-layout-compose{% endif %}">
|
|
{# ── Folders ── #}
|
|
<nav class="mail-folders" aria-label="Папки">
|
|
{% for key, label in folder_choices %}
|
|
<a href="/email/?folder={{ key }}{% if search_q %}&q={{ search_q|urlencode }}{% endif %}"
|
|
hx-get="/email/?folder={{ key }}{% if search_q %}&q={{ search_q|urlencode }}{% endif %}" hx-target="#main-content" hx-push-url="/email/?folder={{ key }}{% if search_q %}&q={{ search_q|urlencode }}{% endif %}" hx-indicator="#loader"
|
|
class="mail-folder{% if active_folder == key %} is-active{% endif %}">
|
|
<span class="mail-folder-icon">
|
|
{% if key == 'inbox' %}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>
|
|
{% elif key == 'sent' %}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>
|
|
{% elif key == 'drafts' %}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
|
|
{% elif key == 'trash' %}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
|
|
{% else %}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
|
{% endif %}
|
|
</span>
|
|
<span class="mail-folder-label">{{ label }}</span>
|
|
{% if key == 'inbox' and unread_count %}
|
|
<span class="mail-folder-badge">{{ unread_count }}</span>
|
|
{% endif %}
|
|
</a>
|
|
{% endfor %}
|
|
</nav>
|
|
|
|
{# ── Message list ── #}
|
|
<section class="mail-list" aria-label="Список листів">
|
|
<div class="mail-list-head">
|
|
<span>
|
|
{% for key, label in folder_choices %}{% if active_folder == key %}{{ label }}{% endif %}{% endfor %}
|
|
{% if unread_count and active_folder == 'inbox' %}<em>{{ unread_count }}</em>{% endif %}
|
|
</span>
|
|
</div>
|
|
<div class="mail-list-scroll">
|
|
{% if messages %}
|
|
{% for msg in messages %}
|
|
{% include "partials/email-list-row.html" %}
|
|
{% endfor %}
|
|
{% if mail_has_more %}
|
|
<div id="mail-load-more-wrap" class="mail-load-more-wrap">
|
|
<button type="button"
|
|
class="mail-btn mail-btn-ghost mail-load-more-btn"
|
|
hx-get="/email/more/?folder={{ active_folder }}&offset={{ mail_next_offset }}{% if search_q %}&q={{ search_q|urlencode }}{% endif %}"
|
|
hx-target="#mail-load-more-wrap"
|
|
hx-swap="outerHTML"
|
|
hx-indicator="#loader">
|
|
Завантажити ще…
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="mail-list-empty">
|
|
{% if search_q %}Нічого не знайдено.{% else %}Немає листів у цій папці.{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
|
|
{# ── Reader / Compose ── #}
|
|
<section class="mail-reader" aria-label="Лист">
|
|
{% if compose %}
|
|
<div class="mail-compose">
|
|
<div class="mail-reader-head">
|
|
<h2>Новий лист</h2>
|
|
<a href="/email/?folder={{ active_folder }}"
|
|
hx-get="/email/?folder={{ active_folder }}" hx-target="#main-content" hx-push-url="/email/?folder={{ active_folder }}" hx-indicator="#loader"
|
|
class="mail-btn mail-btn-ghost">Скасувати</a>
|
|
</div>
|
|
{% if send_errors %}
|
|
<div class="mail-alert mail-alert-error">{% for err in send_errors %}<div>{{ err }}</div>{% endfor %}</div>
|
|
{% endif %}
|
|
{% if send_success %}
|
|
<div class="mail-alert mail-alert-ok">Лист відправлено.</div>
|
|
{% endif %}
|
|
<form class="mail-compose-form"
|
|
hx-post="/email/send/"
|
|
hx-target="#main-content"
|
|
hx-encoding="multipart/form-data"
|
|
hx-indicator="#loader"
|
|
enctype="multipart/form-data">
|
|
{% csrf_token %}
|
|
<label class="mail-field"><span>Кому</span>
|
|
<input type="text" name="to" value="{{ form_to|default:'' }}" required placeholder="email@example.com">
|
|
</label>
|
|
<label class="mail-field"><span>Копія</span>
|
|
<input type="text" name="cc" value="{{ form_cc|default:'' }}">
|
|
</label>
|
|
<label class="mail-field"><span>Прихована</span>
|
|
<input type="text" name="bcc" value="{{ form_bcc|default:'' }}" placeholder="bcc@example.com">
|
|
</label>
|
|
<label class="mail-field"><span>Тема</span>
|
|
<input type="text" name="subject" value="{{ form_subject|default:'' }}" required>
|
|
</label>
|
|
<textarea name="body" required rows="14" placeholder="Текст листа…">{{ form_body|default:'' }}</textarea>
|
|
<div class="mail-attach">
|
|
<label class="mail-attach-label">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
|
<span>Прикріпити файли</span>
|
|
<input type="file" name="attachments" multiple
|
|
class="mail-attach-input"
|
|
accept="*/*"
|
|
data-mail-attach>
|
|
</label>
|
|
<div class="mail-attach-hint">До 5 файлів, кожен ≤ 10 МБ</div>
|
|
<ul class="mail-attach-list" data-mail-attach-list hidden></ul>
|
|
</div>
|
|
<div class="mail-compose-actions">
|
|
<button type="submit" class="mail-btn mail-btn-compose">Надіслати</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{% elif view == 'read' and message %}
|
|
<div class="mail-message">
|
|
<div class="mail-reader-head">
|
|
<h2>{{ message.subject|default:"(без теми)" }}</h2>
|
|
</div>
|
|
<div class="mail-message-meta">
|
|
<div><strong>Від:</strong> {{ message.sender }}</div>
|
|
<div><strong>Кому:</strong> {{ message.to }}</div>
|
|
{% if message.date %}<div><strong>Дата:</strong> {{ message.date }}</div>{% endif %}
|
|
</div>
|
|
<div class="mail-message-body">
|
|
{% if message.body_html %}
|
|
<iframe class="mail-html-frame"
|
|
title="{{ message.subject|default:'Лист' }}"
|
|
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin"
|
|
referrerpolicy="no-referrer"
|
|
scrolling="yes"
|
|
srcdoc="{{ message.body_html|force_escape }}"></iframe>
|
|
{% elif message.body %}
|
|
<div class="mail-message-text">{{ message.body|linebreaksbr }}</div>
|
|
{% else %}
|
|
<div class="mail-message-text" style="color:var(--color-muted-foreground);">Порожній лист</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if message.attachments %}
|
|
<div class="mail-attachments">
|
|
<div class="mail-attachments-title">Вкладення</div>
|
|
<div class="mail-attachments-list">
|
|
{% for att in message.attachments %}
|
|
<a href="/email/attachment/?folder={{ active_folder }}&uid={{ message.uid }}&i={{ att.index }}" class="mail-attachment">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
|
{{ att.name }}
|
|
{% if att.size %}<span>{{ att.size|filesizeformat }}</span>{% endif %}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if send_success %}
|
|
<div class="mail-alert mail-alert-ok">Відповідь надіслано.</div>
|
|
{% endif %}
|
|
{% if send_errors %}
|
|
<div class="mail-alert mail-alert-error">{% for err in send_errors %}<div>{{ err }}</div>{% endfor %}</div>
|
|
{% endif %}
|
|
|
|
{% if message.reply_to %}
|
|
<form class="mail-reply" method="post" action="/email/send/"
|
|
hx-post="/email/send/" hx-target="#main-content" hx-indicator="#loader">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="to" value="{{ message.reply_to }}">
|
|
<input type="hidden" name="subject" value="{{ message.reply_subject }}">
|
|
<input type="hidden" name="reply_uid" value="{{ message.uid }}">
|
|
<input type="hidden" name="reply_folder" value="{{ active_folder }}">
|
|
<div class="mail-reply-label">Відповідь для <strong>{{ message.reply_to }}</strong></div>
|
|
<textarea name="body" required rows="4" placeholder="Напишіть відповідь…">{{ form_body|default:'' }}</textarea>
|
|
<button type="submit" class="mail-btn mail-btn-compose">Надіслати відповідь</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% elif view == 'read' and read_error %}
|
|
<div class="mail-reader-empty mail-alert mail-alert-error">{{ read_error }}</div>
|
|
|
|
{% elif send_success %}
|
|
<div class="mail-reader-empty">
|
|
<div class="mail-alert mail-alert-ok" style="max-width:20rem;">Лист відправлено.</div>
|
|
</div>
|
|
|
|
{% else %}
|
|
<div class="mail-reader-empty">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" opacity="0.35"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 7L2 7"/></svg>
|
|
<p>Оберіть лист зі списку</p>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
function formatSize(n) {
|
|
if (n < 1024) return n + ' B';
|
|
if (n < 1048576) return (n / 1024).toFixed(1) + ' КБ';
|
|
return (n / 1048576).toFixed(1) + ' МБ';
|
|
}
|
|
function bindAttach(root) {
|
|
root.querySelectorAll('[data-mail-attach]').forEach(function (input) {
|
|
if (input.dataset.bound) return;
|
|
input.dataset.bound = '1';
|
|
input.addEventListener('change', function () {
|
|
var list = input.closest('.mail-attach') && input.closest('.mail-attach').querySelector('[data-mail-attach-list]');
|
|
if (!list) return;
|
|
list.innerHTML = '';
|
|
var files = Array.prototype.slice.call(input.files || []);
|
|
if (!files.length) { list.hidden = true; return; }
|
|
files.forEach(function (f) {
|
|
var li = document.createElement('li');
|
|
li.textContent = f.name + ' ';
|
|
var span = document.createElement('span');
|
|
span.textContent = formatSize(f.size);
|
|
li.appendChild(span);
|
|
list.appendChild(li);
|
|
});
|
|
list.hidden = false;
|
|
});
|
|
});
|
|
}
|
|
bindAttach(document);
|
|
document.body.addEventListener('htmx:afterSwap', function (e) {
|
|
bindAttach(e.target || document);
|
|
});
|
|
})();
|
|
</script>
|
|
{% endif %}
|