328 lines
19 KiB
HTML
328 lines
19 KiB
HTML
{{define "webmail_certs.html"}}
|
|
<!DOCTYPE html>
|
|
<html lang="en" data-bs-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Certs - Webmail</title>
|
|
<link href="/webmail/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="/webmail/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
|
|
<style>
|
|
body { background-color: #1a1a1a; color: #e0e0e0; }
|
|
.card { background-color: #2d2d2d; border: 1px solid #404040; }
|
|
.table-dark { --bs-table-bg: #2d2d2d; --bs-table-border-color: #404040; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{{template "csrf_script" .}}
|
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
|
<div class="container-fluid">
|
|
<span class="navbar-brand mb-0 h1"><i class="bi bi-inbox-fill me-2"></i>Webmail <small class="text-muted">{{.mailbox.Email}}</small></span>
|
|
<div class="navbar-nav flex-row gap-2 ms-auto">
|
|
<a href="/webmail/mail/INBOX" class="btn btn-outline-light btn-sm"><i class="bi bi-envelope me-1"></i>Mail</a>
|
|
<button type="button" onclick="openCompose('/webmail/mail/compose')" class="btn btn-outline-light btn-sm"><i class="bi bi-pencil-square me-1"></i>Compose</button>
|
|
<a href="/webmail/rules" class="btn btn-outline-light btn-sm"><i class="bi bi-funnel me-1"></i>Rules</a>
|
|
<a href="/webmail/certs" class="btn btn-light btn-sm"><i class="bi bi-shield-lock me-1"></i>Certs</a>
|
|
<a href="/webmail/account" class="btn btn-outline-light btn-sm"><i class="bi bi-gear me-1"></i>Account</a>
|
|
<form method="post" action="/webmail/logout" class="d-inline">
|
|
<button type="submit" class="btn btn-outline-light btn-sm"><i class="bi bi-box-arrow-right me-1"></i>Sign out</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1090;">
|
|
{{range .flashes}}
|
|
<div class="toast align-items-center text-bg-{{if eq .Category "error"}}danger{{else}}{{.Category}}{{end}} border-0" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="false">
|
|
<div class="d-flex">
|
|
<div class="toast-body">
|
|
<i class="bi bi-{{if eq .Category "error"}}exclamation-triangle{{else if eq .Category "success"}}check-circle{{else}}info-circle{{end}} me-2"></i>
|
|
{{.Message}}
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<div class="container pb-5">
|
|
<h4 class="mb-4"><i class="bi bi-shield-lock me-2"></i>Certs</h4>
|
|
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle me-2"></i>Two separate systems live here, each doing one job: <strong>S/MIME certificates sign</strong> outgoing mail (proves it came from you and wasn't altered) — <strong>PGP keys encrypt</strong> it (only the recipient can read it). They're different standards with different key formats; a message can use either, both, or neither. PGP private keys are protected by their own passphrase (never stored anywhere), so you'll be asked for it the first time you use one each session.
|
|
</div>
|
|
|
|
<h5 class="mb-3"><i class="bi bi-pen me-2"></i>S/MIME Certificates <small class="text-muted fs-6">— for signing</small></h5>
|
|
|
|
<div class="card mb-4">
|
|
<div class="card-header"><h6 class="mb-0"><i class="bi bi-person-badge me-2"></i>Your Certificates</h6></div>
|
|
<div class="card-body">
|
|
{{if .identities}}
|
|
<div class="table-responsive mb-4">
|
|
<table class="table table-dark table-hover mb-0">
|
|
<thead><tr><th>Expires</th><th>Actions</th></tr></thead>
|
|
<tbody>
|
|
{{range .identities}}
|
|
<tr>
|
|
<td>{{.NotAfter.Format "2006-01-02"}}</td>
|
|
<td class="d-flex gap-2">
|
|
<a href="/webmail/smime/identity/{{.ID}}/download" class="btn btn-outline-light btn-sm"><i class="bi bi-download me-1"></i>Download</a>
|
|
<form method="post" action="/webmail/smime/identity/{{.ID}}/remove" class="d-inline">
|
|
<button type="submit" class="btn btn-outline-danger btn-sm" data-confirm="Remove this S/MIME certificate? Mail signed with it will no longer verify."><i class="bi bi-trash me-1"></i>Remove</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{else}}
|
|
<p class="text-muted">No S/MIME certificates yet. Generate a free self-signed certificate, or import one you already have (.p12/.pfx).</p>
|
|
{{end}}
|
|
|
|
<hr class="my-4">
|
|
|
|
<div class="row g-4">
|
|
<div class="col-md-6">
|
|
<h6>Generate New</h6>
|
|
<form method="post" action="/webmail/smime/identity/generate" class="row g-2">
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-magic me-1"></i>Generate Self-Signed Certificate</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6>Import Existing (.p12 / .pfx)</h6>
|
|
<form method="post" action="/webmail/smime/identity/import" enctype="multipart/form-data" class="row g-2">
|
|
<div class="col-12">
|
|
<input type="file" class="form-control form-control-sm" name="p12_file" accept=".p12,.pfx" required>
|
|
</div>
|
|
<div class="col-12">
|
|
<input type="password" class="form-control form-control-sm" name="p12_password" placeholder=".p12 export password (if any)">
|
|
</div>
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-secondary btn-sm">Import</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-5">
|
|
<div class="card-header"><h6 class="mb-0"><i class="bi bi-people me-2"></i>S/MIME Contact Certificates</h6></div>
|
|
<div class="card-body">
|
|
<form method="post" action="/webmail/smime/contacts/add" enctype="multipart/form-data" class="row g-2 align-items-end mb-4">
|
|
<div class="col-auto">
|
|
<label class="form-label">Email</label>
|
|
<input type="email" class="form-control form-control-sm" name="email" placeholder="someone@example.com" required>
|
|
</div>
|
|
<div class="col-auto">
|
|
<label class="form-label">Certificate (.pem/.crt/.cer)</label>
|
|
<input type="file" class="form-control form-control-sm" name="cert_file" accept=".pem,.crt,.cer" required>
|
|
</div>
|
|
<div class="col-auto">
|
|
<button type="submit" class="btn btn-success btn-sm"><i class="bi bi-plus-circle me-1"></i>Add Contact</button>
|
|
</div>
|
|
</form>
|
|
|
|
{{if .contacts}}
|
|
<div class="table-responsive">
|
|
<table class="table table-dark table-hover mb-0">
|
|
<thead><tr><th>Email</th><th>Added</th><th>Actions</th></tr></thead>
|
|
<tbody>
|
|
{{range .contacts}}
|
|
<tr>
|
|
<td>{{.Email}}</td>
|
|
<td>{{.CreatedAt.Format "2006-01-02"}}</td>
|
|
<td>
|
|
<form method="post" action="/webmail/smime/contacts/{{.ID}}/remove" class="d-inline">
|
|
<button type="submit" class="btn btn-outline-danger btn-sm" title="Remove" data-confirm="Remove this contact's certificate?"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{else}}
|
|
<p class="text-muted mb-0">No contact certificates yet. They're also captured automatically when you open a validly signed email from someone new.</p>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
|
|
<h5 class="mb-3"><i class="bi bi-lock me-2"></i>PGP Keys <small class="text-muted fs-6">— for encryption</small></h5>
|
|
|
|
<div class="card mb-4">
|
|
<div class="card-header"><h6 class="mb-0"><i class="bi bi-key me-2"></i>Your Keys</h6></div>
|
|
<div class="card-body">
|
|
{{if .pgp_identities}}
|
|
<div class="table-responsive mb-4">
|
|
<table class="table table-dark table-hover mb-0">
|
|
<thead><tr><th>Label</th><th>Fingerprint</th><th>Status</th><th>Actions</th></tr></thead>
|
|
<tbody>
|
|
{{range .pgp_identities}}
|
|
<tr>
|
|
<td>{{if .Label}}{{.Label}}{{else}}<span class="text-muted">(no label)</span>{{end}}</td>
|
|
<td><code class="small">{{.Fingerprint}}</code></td>
|
|
<td>{{if index $.pgp_unlocked .ID}}<span class="badge bg-success">Unlocked this session</span>{{else}}<span class="badge bg-secondary">Locked</span>{{end}}</td>
|
|
<td class="d-flex gap-2">
|
|
<a href="/webmail/pgp/identity/{{.ID}}/download" class="btn btn-outline-light btn-sm"><i class="bi bi-download me-1"></i>Download Public Key</a>
|
|
<form method="post" action="/webmail/pgp/identity/{{.ID}}/remove" class="d-inline">
|
|
<button type="submit" class="btn btn-outline-danger btn-sm" data-confirm="Remove this PGP key? Mail encrypted to it will no longer decrypt."><i class="bi bi-trash me-1"></i>Remove</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{else}}
|
|
<p class="text-muted">No PGP keys yet. Generate a new keypair, or import one you already have (an ASCII-armored .asc export from e.g. GnuPG).</p>
|
|
{{end}}
|
|
|
|
<hr class="my-4">
|
|
|
|
<div class="row g-4">
|
|
<div class="col-md-6">
|
|
<h6>Generate New</h6>
|
|
<form method="post" action="/webmail/pgp/identity/generate" class="row g-2">
|
|
<div class="col-12">
|
|
<input type="text" class="form-control form-control-sm" name="label" placeholder="Label (e.g. "Work") — optional, helps tell keys apart">
|
|
</div>
|
|
<div class="col-12">
|
|
<input type="password" class="form-control form-control-sm" name="passphrase" placeholder="Choose a passphrase (min 8 chars)" required minlength="8">
|
|
</div>
|
|
<div class="col-12">
|
|
<input type="password" class="form-control form-control-sm" name="passphrase_confirm" placeholder="Confirm passphrase" required minlength="8">
|
|
</div>
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-magic me-1"></i>Generate PGP Key</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6>Import Existing (.asc)</h6>
|
|
<form method="post" action="/webmail/pgp/identity/import" enctype="multipart/form-data" class="row g-2">
|
|
<div class="col-12">
|
|
<input type="text" class="form-control form-control-sm" name="label" placeholder="Label — optional">
|
|
</div>
|
|
<div class="col-12">
|
|
<input type="file" class="form-control form-control-sm" name="key_file" accept=".asc,.pem,.gpg" required>
|
|
</div>
|
|
<div class="col-12">
|
|
<input type="password" class="form-control form-control-sm" name="passphrase" placeholder="The key's passphrase (its own, or a new one if it has none)" required>
|
|
</div>
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-secondary btn-sm">Import</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header"><h6 class="mb-0"><i class="bi bi-people me-2"></i>PGP Contact Keys</h6></div>
|
|
<div class="card-body">
|
|
<form method="post" action="/webmail/pgp/contacts/add" enctype="multipart/form-data" class="row g-2 align-items-end mb-4">
|
|
<div class="col-auto">
|
|
<label class="form-label">Email</label>
|
|
<input type="email" class="form-control form-control-sm" name="email" placeholder="someone@example.com" required>
|
|
</div>
|
|
<div class="col-auto">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control form-control-sm" name="label" placeholder="optional">
|
|
</div>
|
|
<div class="col-auto">
|
|
<label class="form-label">Public Key (.asc)</label>
|
|
<input type="file" class="form-control form-control-sm" name="key_file" accept=".asc,.pem,.gpg" required>
|
|
</div>
|
|
<div class="col-auto">
|
|
<button type="submit" class="btn btn-success btn-sm"><i class="bi bi-plus-circle me-1"></i>Add Contact</button>
|
|
</div>
|
|
</form>
|
|
|
|
{{if .pgp_contacts}}
|
|
<div class="table-responsive">
|
|
<table class="table table-dark table-hover mb-0">
|
|
<thead><tr><th>Email</th><th>Label</th><th>Fingerprint</th><th>Added</th><th>Actions</th></tr></thead>
|
|
<tbody>
|
|
{{range .pgp_contacts}}
|
|
<tr>
|
|
<td>{{.Email}}</td>
|
|
<td>{{.Label}}</td>
|
|
<td><code class="small">{{.Fingerprint}}</code></td>
|
|
<td>{{.CreatedAt.Format "2006-01-02"}}</td>
|
|
<td>
|
|
<form method="post" action="/webmail/pgp/contacts/{{.ID}}/remove" class="d-inline">
|
|
<button type="submit" class="btn btn-outline-danger btn-sm" title="Remove" data-confirm="Remove this contact's PGP key?"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{else}}
|
|
<p class="text-muted mb-0">No contact keys yet. Ask a sender for their public key export, or grab it from a keyserver, and add it here before you can encrypt mail to them.</p>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="confirmationModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-question-circle me-2"></i>Confirm Action</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body" id="confirmationModalBody">Are you sure you want to proceed?</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-danger" id="confirmationModalConfirm">Confirm</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{template "compose_widget" .}}
|
|
|
|
<script src="/webmail/static/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.querySelectorAll('.toast').forEach(function(el) { new bootstrap.Toast(el, {delay: 5000}).show(); });
|
|
});
|
|
function showConfirmation(message) {
|
|
return new Promise((resolve) => {
|
|
const modal = document.getElementById('confirmationModal');
|
|
document.getElementById('confirmationModalBody').textContent = message;
|
|
const confirmButton = document.getElementById('confirmationModalConfirm');
|
|
const handleConfirm = () => { resolve(true); bootstrap.Modal.getInstance(modal).hide(); cleanup(); };
|
|
const handleCancel = () => { resolve(false); cleanup(); };
|
|
const cleanup = () => {
|
|
confirmButton.removeEventListener('click', handleConfirm);
|
|
modal.removeEventListener('hidden.bs.modal', handleCancel);
|
|
};
|
|
confirmButton.addEventListener('click', handleConfirm);
|
|
modal.addEventListener('hidden.bs.modal', handleCancel, { once: true });
|
|
new bootstrap.Modal(modal).show();
|
|
});
|
|
}
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.querySelectorAll('[data-confirm]').forEach(function(button) {
|
|
button.addEventListener('click', async function(e) {
|
|
e.preventDefault();
|
|
if (await showConfirmation(this.getAttribute('data-confirm'))) {
|
|
const form = this.closest('form');
|
|
if (form) form.submit();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|