Browse Source

Email testing panel is now working so you can easily send test emails.

master
Zed A. Shaw 1 month ago
parent
commit
70a1dc0ae4
  1. 149
      api/admin/email.js
  2. 2
      client/pages/admin/EmailConfig.svelte
  3. 70
      client/pages/admin/EmailSend.svelte
  4. 1
      emails/test.html
  5. 1
      emails/test.txt
  6. 48
      lib/email.js

149
api/admin/email.js

@ -0,0 +1,149 @@
import logging from '../../lib/logging.js';
import assert from 'assert';
import { API } from '../../lib/api.js';
import { company } from '../../emails/config.js';
import { get_config, dns_check, send_email, load_templates } from '../../lib/email.js';
const log = logging.create(import.meta.url);
const ERRORS = {
ip4_double_reverse: {
id: 0,
title: "No IP4 Double Reverse",
active: false,
text: "IPv4 addresses need to have both a reverse DNS record and a matching DNS record for the host. You have to add a reverse DNS for the IP Host address and then make sure that exact IPv4 address is listed in your DNS with a A record."
},
ip6_double_reverse: {
id: 1,
title: "No IP6 Double Reverse",
active: false,
text: "IPv6 addresses need to have both a reverse DNS record and a matching DNS record for the host. You have to add a reverse DNS for the IP Host address and then make sure that exact IPv6 address is listed in your DNS with a AAAA record (not A)."
},
no_mx_record: {
id: 2,
title: "No MX Record",
active: false,
text: "You can run your server without an MX record but it makes it less reliable. Add a single record with your main hostname and a 10 Priority."
},
spf_invalid: {
id: 3,
title: "SPF Record Invalid",
active: false,
text: "Your SPF record needs to mention both the IPv4 address with ip4:X and IPv6 address with ip6:X."
},
no_dmarc: {
id: 4,
title: "No DMARC Record",
active: false,
text: "You need a DMARC record, and it's recommended you configure it to p=none but route the errors to an email address on this server so you can monitor failures."
},
no_ip4_address: {
id: 5,
title: "No IP4 Address",
active: false,
text: "Most email providers are now requiring an IPv4 address that is also double reverse lookup, which means you need an IPv4 A record and a reverse DNS for it that exactly matches."
},
no_ip6_address: {
id: 6,
title: "No IP6 Address",
active: false,
text: "Most email providers are now requiring an IPv6 address that is also double reverse lookup, which means you need an IPv6 AAAA record and a reverse DNS for it that exactly matches."
},
no_spf: {
id: 7,
title: "No SPF Record",
active: false,
text: "You need an SPF record. It should be a TXT record and list your IP4 and IP6 addresses in the format \"v=spf1 a mx ip4:X ip6:Y ~all\"."
},
}
const analyze_dns = (dns) => {
let r = [];
// consider using a Rools engine if this gets too insane
if(dns.ip4.error) {
r.push(ERRORS.no_ip4_address);
r.push(ERRORS.ip4_double_reverse);
}
if(dns.ip4.reverse_errors) {
r.push(ERRORS.ip4_double_reverse);
}
if(dns.ip6.error) {
r.push(ERRORS.no_ip6_address);
r.push(ERRORS.ip6_double_reverse);
}
if(dns.ip6.reverse_errors) {
r.push(ERRORS.ip6_double_reverse);
}
if(dns.mx_error) {
r.push(ERRORS.no_mx_record);
}
if(dns.spf_error) {
r.push(ERRORS.no_spf);
}
if(dns.dmarc_error) {
r.push(ERRORS.no_dmarc);
}
if(r.length > 1) r[0].active = true;
return r;
}
export const get = async (req, res) => {
const api = new API(req, res);
const { domain_name } = req.query;
try {
assert(domain_name, "domain name is required");
const dns = await dns_check(domain_name);
const tests = analyze_dns(dns);
log.debug(dns);
api.reply(200, {dns, tests});
} catch (error) {
log.error(error);
api.error(500, error.message || "Internal Server Error");
}
}
const send_test = async (email) => {
try {
const test_email = await load_templates("test");
const text = test_email.txt(email);
const html = test_email.html(email);
return send_email({
from: company.mail,
to: email,
subject: `Test email for ${company.website}`,
text, html
});
} catch(error) {
log.error(error);
return error;
}
}
export const post = async (req, res) => {
const api = new API(req, res);
try {
log.debug(`Sending test email to ${req.body.to_address}`);
const error = await send_test(req.body.to_address);
api.reply(200, {
message: "Email sent. Check server logs.",
error: error === undefined ? error : error.message,
config: get_config()
});
} catch (error) {
log.error(error);
api.error(500, error.message || "Internal Server Error");
}
}

2
client/pages/admin/EmailConfig.svelte

@ -9,8 +9,8 @@
export let params = {};
let panels = [
{title: "DNS Test", active: false, icon: "cast", component: EmailDNS },
{title: "Send Test", active: false, icon: "send", component: EmailSend },
{title: "DNS Test", active: false, icon: "cast", component: EmailDNS },
]
const select_named = () => {

70
client/pages/admin/EmailSend.svelte

@ -1,56 +1,40 @@
<script>
import { onMount } from 'svelte';
import api from "$/client/api.js";
import FormField, { validate } from '$/client/components/FormField.svelte';
let form = {
from_address: "",
from_name: "",
to_address: "",
errors: { main: "" },
}
let rules = {
from_address: "required|email",
to_address: "required|email",
}
api.mock({
"/api/admin/email": {
"get": [200, {"message": "OK" }],
}
});
let error;
let message;
let config;
const run_test = async () => {
const [status, data] = await api.post(`/api/admin/email`);
const send_test = async () => {
form = validate(form, rules);
if(form.valid) {
const [status, data] = await api.post(`/api/admin/email`, form);
if(status !== 200) {
console.error(status, data);
}
if(status === 200) {
return data;
message = data.message;
error = data.error;
config = data.config;
} else {
console.error("Invalid response", status, data);
return data;
console.log("Invalid form", form);
}
}
onMount(async () => run_test());
</script>
<form>
<card>
<top>
<h1>Send Test</h1>
<error>{ form.errors.main }</error>
</top>
</card>
<middle>
<FormField form={ form } field="from_address" label="From Email">
<input type="text" id="from_address" bind:value={form.from_address } name="from_address">
</FormField>
<FormField form={ form } field="from_name" label="From Name">
<input type="text" id="from_name" bind:value={form.from_name } name="from_name">
</FormField>
<FormField form={ form } field="to_address" label="To Email">
<input type="text" id="to_address" bind:value={form.to_address } name="to_address">
</FormField>
@ -58,16 +42,34 @@
<bottom>
<button-group>
<button data-testid="register-button" on:click|preventDefault={ () => form = validate(form, rules) }>Send</button>
<button data-testid="register-button" on:click|preventDefault={ () => send_test() }>Send</button>
</button-group>
</bottom>
</card>
</form>
<h2>OpenSMTPD Debug Logs</h2>
{#if message}
<br/>
<callout class="info">
<span>{ message }</span>
</callout>
{/if}
{#if error}
<h2>Error Results</h2>
<pre>
<code>
<span>Logs from smtpd here</span>
<span class="error">With errors flagged</span>
{ error }
</code>
</pre>
{/if}
{#if config}
<h2>Mail Server Config</h2>
<pre>
<code>
{JSON.stringify(config, null, 2)}
</code>
</pre>
{/if}

1
emails/test.html

@ -0,0 +1 @@
<h1>Test HTML Email</h1>

1
emails/test.txt

@ -0,0 +1 @@
Test text email.

48
lib/email.js

@ -6,22 +6,30 @@ import { promises } from "dns";
const log = logging.create("lib/email.js");
// TODO: move this out to an easy to access/change location
let configuration = {};
const configure_transporter = () => {
if(process.env.DANGER_ADMIN || process.env.DEBUG) {
return nodemailer.createTransport({
configuration = {
streamTransport: true,
newline: 'windows'
});
}
} else {
return nodemailer.createTransport({
configuration = {
pool: true,
host: "localhost",
port: 25,
secure: false, // use TLS
});
}
}
return nodemailer.createTransport(configuration);
}
// TODO: trash hacks to get the config out for the mail testing tool
export const get_config = () => configuration;
export const transporter = configure_transporter();
export const load_templates = async (name) => {
@ -42,13 +50,33 @@ export const debug_templates = async (templates, name) => {
}
}
export const send_email = (data) => {
transporter.sendMail(data, (err, info) => {
if(err) log.error(err);
log.debug(info.envelope);
log.debug(info.messageId);
// info.message.pipe(process.stdout);
/* This never rejects the promise it makes but instead returns
* any errors it receives or undefined if none.
*/
export const send_email = async (data) => {
const result = new Promise((resolve, reject) => {
transporter.sendMail(data, (err, info) => {
try {
if(err) {
log.error(err);
resolve(err);
} else {
log.debug(info.envelope);
log.debug(info.messageId);
if(process.env.DEBUG) {
info.message.pipe(process.stdout);
}
resolve(undefined);
}
} catch(error) {
resolve(error);
}
});
});
return result;
}
const add_reverse_error = (result, error) => {

Loading…
Cancel
Save