🌊 TeamFlow — Modern Trello alternative with email integration
- Full-stack: React 18 + Express + SQLite - Drag-and-drop kanban boards with @hello-pangea/dnd - Google App Password email integration (SMTP + IMAP) - Inbound email: create cards by sending emails - Reply-to-card: email replies become comments - Admin/user management with role-based access - Setup wizard: email config → admin creation - Checklists, time tracking, priorities, labels, due dates - Real-time notifications with activity feed - Beautiful HTML email templates
This commit is contained in:
110
server/email/transporter.js
Normal file
110
server/email/transporter.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import db from '../db.js';
|
||||
|
||||
let transporter = null;
|
||||
|
||||
export function getEmailConfig() {
|
||||
return db.prepare('SELECT * FROM email_config WHERE id = 1').get();
|
||||
}
|
||||
|
||||
export async function initTransporter() {
|
||||
const config = getEmailConfig();
|
||||
if (!config) return null;
|
||||
try {
|
||||
transporter = nodemailer.createTransport({
|
||||
host: config.smtp_host,
|
||||
port: config.smtp_port,
|
||||
secure: config.smtp_port === 465,
|
||||
auth: {
|
||||
user: config.email,
|
||||
pass: config.app_password,
|
||||
},
|
||||
});
|
||||
await transporter.verify();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Email transporter init failed:', err.message);
|
||||
transporter = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function testConnection(host, port, email, password) {
|
||||
const t = nodemailer.createTransport({
|
||||
host,
|
||||
port,
|
||||
secure: port === 465,
|
||||
auth: { user: email, pass: password },
|
||||
});
|
||||
await t.verify();
|
||||
await t.close();
|
||||
}
|
||||
|
||||
export async function sendMail({ to, subject, html, replyTo, inReplyTo, references }) {
|
||||
if (!transporter) await initTransporter();
|
||||
if (!transporter) throw new Error('Email not configured');
|
||||
const config = getEmailConfig();
|
||||
const msg = {
|
||||
from: `"TeamFlow" <${config.email}>`,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
headers: {},
|
||||
};
|
||||
if (replyTo) msg.replyTo = replyTo;
|
||||
if (inReplyTo) msg.headers['In-Reply-To'] = inReplyTo;
|
||||
if (references) msg.headers.References = references;
|
||||
const info = await transporter.sendMail(msg);
|
||||
db.prepare(`INSERT INTO email_log (from_email, to_email, subject, direction, status, message_id)
|
||||
VALUES (?, ?, ?, 'sent', 'sent', ?)`)
|
||||
.run(config.email, to, subject, info.messageId);
|
||||
return info;
|
||||
}
|
||||
|
||||
export function buildCardUrl(cardId, boardId) {
|
||||
const origin = process.env.APP_URL || 'http://localhost:5173';
|
||||
return `${origin}/board/${boardId}?card=${cardId}`;
|
||||
}
|
||||
|
||||
export function buildCardNotificationHtml({ userName, boardTitle, cardTitle, action, cardUrl, comment }) {
|
||||
const actionMessages = {
|
||||
assigned: `assigned you to a card`,
|
||||
commented: `commented on a card`,
|
||||
moved: `moved a card`,
|
||||
mentioned: `mentioned you in a comment`,
|
||||
due_soon: `has a due date coming up`,
|
||||
};
|
||||
return `<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><style>
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f1f5f9;margin:0;padding:0}
|
||||
.container{max-width:560px;margin:40px auto;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.1)}
|
||||
.header{background:linear-gradient(135deg,#6366f1,#8b5cf6);padding:32px;color:#fff}
|
||||
.header h1{margin:0;font-size:20px;font-weight:600}
|
||||
.header p{margin:8px 0 0;opacity:.9;font-size:14px}
|
||||
.body{padding:32px}
|
||||
.card-preview{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin:16px 0}
|
||||
.card-preview h3{margin:0 0 8px;font-size:16px}
|
||||
.card-preview p{margin:0;color:#64748b;font-size:14px}
|
||||
.comment{background:#f1f5f9;border-radius:8px;padding:16px;margin:16px 0;font-size:14px;line-height:1.6;color:#334155}
|
||||
.btn{display:inline-block;background:#6366f1;color:#fff!important;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:500;margin-top:16px}
|
||||
.footer{padding:20px 32px;background:#f8fafc;border-top:1px solid #e2e8f0;font-size:12px;color:#94a3b8;text-align:center}
|
||||
</style></head><body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>TeamFlow</h1>
|
||||
<p>${userName} ${actionMessages[action] || action}</p>
|
||||
</div>
|
||||
<div class="body">
|
||||
<p style="color:#64748b;margin:0 0 8px">Board: <strong>${boardTitle}</strong></p>
|
||||
<div class="card-preview">
|
||||
<h3>${cardTitle}</h3>
|
||||
${comment ? `<div class="comment">${comment}</div>` : ''}
|
||||
</div>
|
||||
<a href="${cardUrl}" class="btn">View Card</a>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Sent by TeamFlow · <a href="${cardUrl}" style="color:#6366f1">Open in browser</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>`;
|
||||
}
|
||||
Reference in New Issue
Block a user