feat: notifications in the app and, optionally, by e-mail
Derived from events and kept per person: whoever a ticket is assigned to, whoever reports or works on it when it moves, fails QA or gets a comment, and whoever joins a project. Never about what you did yourself, once per event. They arrive live through a subscription to a bell in the header, and are copied by e-mail to whoever turns it on when SMTP_URL is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
MailRecipient,
|
||||
NotificationMailer,
|
||||
} from '@task-stream/api/tracker/application';
|
||||
import { NotificationReadModel } from '@task-stream/api/tracker/domain';
|
||||
import { createTransport, Transporter } from 'nodemailer';
|
||||
|
||||
/**
|
||||
* E-mail copies through SMTP_URL (`smtps://user:pass@smtp.example.org`).
|
||||
* Without it nothing is sent and nobody is offered e-mail.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SmtpNotificationMailer extends NotificationMailer {
|
||||
private readonly logger = new Logger(SmtpNotificationMailer.name);
|
||||
private readonly transport: Transporter | null;
|
||||
private readonly from: string;
|
||||
private readonly webUrl: string;
|
||||
|
||||
constructor(config: ConfigService) {
|
||||
super();
|
||||
const url = config.get<string>('SMTP_URL');
|
||||
this.transport = url ? createTransport(url) : null;
|
||||
this.from =
|
||||
config.get<string>('MAIL_FROM') || 'TaskStream <no-reply@pireon.dev>';
|
||||
this.webUrl = (config.get<string>('WEB_URL') || '').replace(/\/+$/, '');
|
||||
if (this.transport) this.logger.log('E-mail notifications enabled');
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this.transport !== null;
|
||||
}
|
||||
|
||||
async send(
|
||||
to: MailRecipient,
|
||||
notification: NotificationReadModel,
|
||||
): Promise<void> {
|
||||
if (!this.transport) return;
|
||||
const link = this.linkTo(notification);
|
||||
const subject = notification.issueKey
|
||||
? `[${notification.issueKey}] ${notification.summary}`
|
||||
: `[${notification.projectKey}] ${notification.summary}`;
|
||||
await this.transport.sendMail({
|
||||
from: this.from,
|
||||
to: { name: to.name, address: to.email },
|
||||
subject: subject.length > 160 ? `${subject.slice(0, 159)}…` : subject,
|
||||
text: [
|
||||
`Hola ${to.name}:`,
|
||||
'',
|
||||
notification.summary,
|
||||
...(link ? ['', link] : []),
|
||||
'',
|
||||
'— TaskStream. Puedes dejar de recibir estos correos desde las notificaciones de la app.',
|
||||
].join('\n'),
|
||||
});
|
||||
}
|
||||
|
||||
private linkTo(notification: NotificationReadModel): string | null {
|
||||
if (!this.webUrl) return null;
|
||||
const project = encodeURIComponent(notification.projectKey);
|
||||
return notification.issueKey
|
||||
? `${this.webUrl}/p/${project}/issues/${encodeURIComponent(notification.issueKey)}`
|
||||
: `${this.webUrl}/p/${project}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user