a fancy canvas mcp server!
at main 172 lines 5.0 kB view raw
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();