a fancy canvas mcp server!
1import nodemailer from "nodemailer";
2import { readFileSync } from "fs";
3import dkim from "nodemailer-dkim";
4
5const SMTP_HOST = process.env.SMTP_HOST;
6const SMTP_PORT = process.env.SMTP_PORT
7 ? parseInt(process.env.SMTP_PORT)
8 : undefined;
9const SMTP_USER = process.env.SMTP_USER;
10const SMTP_PASS = process.env.SMTP_PASS;
11const SMTP_FROM = process.env.SMTP_FROM;
12const BASE_URL = process.env.BASE_URL || "http://localhost:3000";
13
14// DKIM Configuration (optional)
15const DKIM_SELECTOR = process.env.DKIM_SELECTOR;
16const DKIM_DOMAIN = process.env.DKIM_DOMAIN;
17const DKIM_PRIVATE_KEY_FILE = process.env.DKIM_PRIVATE_KEY_FILE;
18
19class Mailer {
20 private transporter: any;
21 private enabled: boolean;
22
23 constructor() {
24 // Check if SMTP is configured
25 if (!SMTP_HOST || !SMTP_PORT || !SMTP_USER || !SMTP_PASS || !SMTP_FROM) {
26 console.warn("SMTP not configured - email functionality disabled");
27 this.enabled = false;
28 return;
29 }
30
31 this.enabled = true;
32
33 // Create SMTP transporter
34 this.transporter = nodemailer.createTransport({
35 host: SMTP_HOST,
36 port: SMTP_PORT,
37 secure: false, // Use STARTTLS
38 auth: {
39 user: SMTP_USER,
40 pass: SMTP_PASS,
41 },
42 });
43
44 // Add DKIM signing if configured
45 if (DKIM_SELECTOR && DKIM_DOMAIN && DKIM_PRIVATE_KEY_FILE) {
46 try {
47 const dkimPrivateKey = readFileSync(DKIM_PRIVATE_KEY_FILE, "utf-8");
48 this.transporter.use(
49 "stream",
50 dkim.signer({
51 domainName: DKIM_DOMAIN,
52 keySelector: DKIM_SELECTOR,
53 privateKey: dkimPrivateKey,
54 headerFieldNames: "from:to:subject:date:message-id",
55 }),
56 );
57 console.log("DKIM signing enabled");
58 } catch (error) {
59 console.warn("DKIM private key not found, emails will not be signed");
60 }
61 }
62 }
63
64 private async sendMail(
65 to: string,
66 subject: string,
67 html: string,
68 text: string,
69 ): Promise<void> {
70 if (!this.enabled) {
71 throw new Error("Email is not configured");
72 }
73
74 await this.transporter.sendMail({
75 from: SMTP_FROM,
76 to,
77 subject,
78 text,
79 html,
80 headers: {
81 "X-Mailer": "Canvas MCP",
82 },
83 });
84 }
85
86 async sendMagicLink(email: string, token: string): Promise<void> {
87 const magicLink = `${BASE_URL}/auth/verify?token=${token}`;
88
89 const html = `<!DOCTYPE html>
90<html>
91<head>
92 <meta charset="utf-8">
93 <meta name="viewport" content="width=device-width, initial-scale=1">
94 <style>
95 img { max-width: 100%; height: auto; }
96 </style>
97</head>
98<body style="font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 40px auto; padding: 20px;">
99 <div>
100 <h1 style="margin-bottom: 20px;">Sign in to Canvas MCP</h1>
101 <p>Click this link to sign in:</p>
102 <p><a href="${magicLink}" style="color: #0066cc;">${magicLink}</a></p>
103 <hr style="margin: 30px 0; border: none; border-top: 1px solid #ddd;">
104 <p style="font-size: 12px; color: #999;">This link expires in 15 minutes. If you didn't request this, ignore it.</p>
105 </div>
106</body>
107</html>`;
108
109 const text = `Sign in to Canvas MCP
110
111Click this link to sign in:
112${magicLink}
113
114This link expires in 15 minutes.
115If you didn't request this, you can safely ignore it.`;
116
117 await this.sendMail(email, "Sign in to Canvas MCP", html, text);
118 }
119
120 async sendOAuthConfirmation(
121 email: string,
122 canvasDomain: string,
123 ): Promise<void> {
124 const html = `<!DOCTYPE html>
125<html>
126<head>
127 <meta charset="utf-8">
128 <meta name="viewport" content="width=device-width, initial-scale=1">
129 <style>
130 img { max-width: 100%; height: auto; }
131 </style>
132</head>
133<body style="font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 40px auto; padding: 20px;">
134 <div>
135 <h1 style="margin-bottom: 20px;">Canvas Account Connected</h1>
136 <div style="background: #d4edda; color: #0a6640; padding: 16px; border-radius: 4px; margin: 20px 0;">
137 Your Canvas account has been successfully connected!
138 </div>
139 <p><strong>Canvas Domain:</strong> <code style="background: #f5f5f5; padding: 2px 6px; border-radius: 3px;">${canvasDomain}</code></p>
140 <p><a href="${BASE_URL}/dashboard" style="color: #0066cc;">View Dashboard →</a></p>
141 <hr style="margin: 30px 0; border: none; border-top: 1px solid #ddd;">
142 <h2 style="font-size: 18px;">Next Steps</h2>
143 <ol style="padding-left: 20px;">
144 <li>Configure Claude Desktop with the MCP server URL</li>
145 <li>Authorize Claude to access your Canvas data</li>
146 <li>Start asking questions about your courses!</li>
147 </ol>
148 </div>
149</body>
150</html>`;
151
152 const text = `Canvas Account Connected!
153
154Your Canvas account (${canvasDomain}) has been successfully connected.
155
156Visit your dashboard: ${BASE_URL}/dashboard
157
158Next Steps:
1591. Configure Claude Desktop with the MCP server URL
1602. Authorize Claude to access your Canvas data
1613. Start asking questions about your courses!`;
162
163 await this.sendMail(
164 email,
165 "Canvas Account Connected - Canvas MCP",
166 html,
167 text,
168 );
169 }
170}
171
172export default new Mailer();