{"record":{"id":"bf17677762ab48c8","repo":"RocketChat/Rocket.Chat","slug":"error-missing-unsubscribe-link","errorCode":"error-missing-unsubscribe-link","errorMessage":"You must provide the [unsubscribe] link.","messagePattern":"You must provide the \\[unsubscribe\\] link\\.","errorType":"error_code","errorClass":"Meteor.Error","httpStatus":400,"severity":"error","filePath":"apps/meteor/server/lib/notifications/mail-messages/functions/sendMail.ts","lineNumber":29,"sourceCode":"import * as Mailer from '../../email/api';\n\nexport const sendMail = async function ({\n\tfrom,\n\tsubject,\n\tbody,\n\tdryrun,\n\tquery,\n}: {\n\tfrom: string;\n\tsubject: string;\n\tbody: string;\n\tdryrun?: boolean;\n\tquery?: string;\n}): Promise<void> {\n\tMailer.checkAddressFormatAndThrow(from, 'Mailer.sendMail');\n\n\tif (body.indexOf('[unsubscribe]') === -1) {\n\t\tthrow new Meteor.Error('error-missing-unsubscribe-link', 'You must provide the [unsubscribe] link.', {\n\t\t\tfunction: 'Mailer.sendMail',\n\t\t});\n\t}\n\n\tlet userQuery: Filter<any> = { 'mailer.unsubscribed': { $exists: 0 } };\n\tif (query) {\n\t\tuserQuery = { $and: [userQuery, EJSON.parse(query)] };\n\t}\n\n\tif (dryrun) {\n\t\tconst user = await Users.findOneByEmailAddress(from);\n\n\t\tif (!user) {\n\t\t\tthrow new Meteor.Error('error-invalid-user', 'Invalid user', {\n\t\t\t\tfunction: 'Mailer.sendMail',\n\t\t\t});\n\t\t}\n","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/lib/notifications/mail-messages/functions/sendMail.ts#L11-L47","documentation":"Mailer.sendMail sends bulk mail and requires the body template to contain the literal [unsubscribe] placeholder so a per-recipient unsubscribe URL can be injected; if body.indexOf('[unsubscribe]') === -1 it throws before querying any users. This is a compliance guard - mass mail must always carry an opt-out link.","triggerScenarios":"Calling Mailer.sendMail({ from, subject, body }) where the body only uses [name]/[email] placeholders, or where an unsubscribe hyperlink was hardcoded instead of using the [unsubscribe] token.","commonSituations":"HTML templates edited under Administration -> Mailer where the placeholder got deleted; templates migrated between workspaces that dropped the token; custom newsletter markup that links to a generic unsubscribe page.","solutions":["Add the [unsubscribe] token where the opt-out belongs, e.g. <a href=\"[unsubscribe]\">Unsubscribe</a>","Save the template and re-run with dryrun: true to confirm the token resolves to a URL","Check the body programmatically for the token before launching a campaign"],"exampleFix":"// before\nconst body = '<p>News!</p><a href=\"https://example.com/unsub\">Unsubscribe</a>';\n\n// after\nconst body = '<p>News!</p><a href=\"[unsubscribe]\">Unsubscribe</a>;","handlingStrategy":"validation","validationCode":"if (!body.includes('[unsubscribe]')) {\n  throw new Error('template is missing the [unsubscribe] placeholder');\n}\nawait Mailer.sendMail({ from, subject, body, dryrun: true });","typeGuard":null,"tryCatchPattern":"try {\n  await Mailer.sendMail({ from, subject, body });\n} catch (err) {\n  if (err instanceof Meteor.Error && err.error === 'error-missing-unsubscribe-link') {\n    // add <a href=\"[unsubscribe]\">Unsubscribe</a> to the template body\n  } else throw err;\n}","preventionTips":["Always author Mailer bodies with the [unsubscribe] token, not a hardcoded link","Dry-run every new/edited template before a real campaign","Add a CI/lint check on stored templates for required placeholders"],"tags":["mailer","email-template","compliance","unsubscribe","rocket-chat"],"backgroundTag":"missing-template-placeholder","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}