Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ bitwarden_license/src/Sso/wwwroot/assets
**/**.swp
.mono
src/Core/MailTemplates/Mjml/out
src/Core/MailTemplates/Mjml/out-hbs

src/Admin/Admin.zip
src/Api/Api.zip
Expand Down
109 changes: 101 additions & 8 deletions src/Core/MailTemplates/Mjml/README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,112 @@
# Email templates
# MJML email templating

This directory contains MJML templates for emails sent by the application. MJML is a markup language designed to reduce the pain of coding responsive email templates.
This directory contains MJML templates for emails. MJML is a markup language designed to reduce the pain of coding responsive email templates. Component based development features in MJML improve code quality and reusability.

## Usage
MJML stands for MailJet Markup Language.

```bash
## Implementation considerations

These `MJML` templates are compiled into HTML which will then be further consumed by our Handlebars mail service. We can continue to use this service to assign values from our View Models. This leverages the existing infrastructure. It also means we can continue to use the double brace (`{{}}`) syntax within MJML since Handlebars can be used to assign values to those `{{variables}}`.

There is no change on how we interact with our view models.

There is an added step where we compile `*.mjml` to `*.html.hbs`. `*.html.hbs` is the format we use so the handlebars service can apply the variables. This build pipeline process is in progress and may need to be manually done at times.

### `*.txt.hbs`

There is no change to how we create the `txt.hbs`. MJML does not impact how we create these artifacts.

## Building `MJML` files

```shell
npm ci

# Build once
# Build *.html to ./out directory
npm run build

# To build on changes
npm run watch
# To build on changes to *.mjml and *.js files, new files will not be tracked, you will need to run again
npm run build:watch

# Build *.html.hbs to ./out directory
npm run build:hbs

# Build minified *.html.hbs to ./out directory
npm run build:minify

# apply prettier formatting
npm run prettier
```

## Development

MJML supports components and you can create your own components by adding them to `.mjmlconfig`.
MJML supports components and you can create your own components by adding them to `.mjmlconfig`. Components are simple JavaScript that return MJML markup based on the attributes assigned, see components/mj-bw-hero.js. The markup is not a proper object, but contained in a string.

When using MJML templating you can use the above [commands](#building-mjml-files) to compile the template and view it in a web browser.

Not all MJML tags have the same attributes, it is highly recommended to review the documentation on the official MJML website to understand the usages of each of the tags.

### Recommended development

#### Mjml email template development

1. create `cool-email.mjml` in appropriate team directory
2. run `npm run build:watch`
3. view compiled `HTML` output in a web browser
4. iterate -> while `build:watch`'ing you should be able to refresh the browser page after the mjml/js re-compile to see the changes

#### Testing with `IMailService`

After the email is developed from the [initial step](#mjml-email-template-development) make sure the email `{{variables}}` are populated properly by running it through an `IMailService` implementation.

1. run `npm run build:minify`
2. copy built `*.html.hbs` files from the build directory to a location the mail service can consume them
3. run code that will send the email

The minified `html.hbs` artifacts are deliverables and must be placed into the correct `src/Core/MailTemplates/Handlebars/` directories in order to be used by `IMailService` implementations.

### Custom tags

There is currently a `mj-bw-hero` tag you can use within your `*.mjml` templates. This is a good example of how to create a component that takes in attribute values allowing us to be more DRY in our development of emails. Since the attribute's input is a string we are able to define whatever we need into the component, in this case `mj-bw-hero`.

In order to view the custom component you have written you will need to include it in the `.mjmlconfig` and reference it in an `mjml` template file.

```html
<!-- Custom component implementation-->
<mj-bw-hero
img-src="https://assets.bitwarden.com/email/v1/business.png"
title="Verify your email to access this Bitwarden Send"
/>
```

Attributes in Custom Components are defined by the developer. They can be required or optional depending on implementation. See the official MJML documentation for more information.

```js
static allowedAttributes = {
"img-src": "string", // REQUIRED: Source for the image displayed in the right-hand side of the blue header area
title: "string", // REQUIRED: large text stating primary purpose of the email
"button-text": "string", // OPTIONAL: text to display in the button
"button-url": "string", // OPTIONAL: URL to navigate to when the button is clicked
"sub-title": "string", // OPTIONAL: smaller text providing additional context for the title
};

static defaultAttributes = {};
```

Custom components, such as `mj-bw-hero`, must be defined in the `.mjmlconfig` in order for them to be compiled and rendered properly in the templates.

```json
{
"packages": ["components/mj-bw-hero"]
}
```

### `mj-include`

You are also able to reference other more static MJML templates in your MJML file simply by referencing the file within the MJML template.

```html
<!-- Example of reference to mjml template -->
<mj-wrapper padding="5px 20px 10px 20px">
<mj-include path="../../components/learn-more-footer.mjml" />
</mj-wrapper>
```
128 changes: 128 additions & 0 deletions src/Core/MailTemplates/Mjml/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
const mjml2html = require("mjml");
const { registerComponent } = require("mjml-core");
const fs = require("fs");
const path = require("path");
const glob = require("glob");

// Parse command line arguments
const args = process.argv.slice(2); // Remove 'node' and script path

// Parse flags
const flags = {
minify: args.includes("--minify") || args.includes("-m"),
watch: args.includes("--watch") || args.includes("-w"),
hbs: args.includes("--hbs") || args.includes("-h"),
trace: args.includes("--trace") || args.includes("-t"),
clean: args.includes("--clean") || args.includes("-c"),
help: args.includes("--help"),
};

// Use __dirname to get absolute paths relative to the script location
const config = {
inputDir: path.join(__dirname, "emails"),
outputDir: path.join(__dirname, "out"),
minify: flags.minify,
validationLevel: "strict",
hbsOutput: flags.hbs,
};

// Debug output
if (flags.trace) {
console.log("[DEBUG] Script location:", __dirname);
console.log("[DEBUG] Input directory:", config.inputDir);
console.log("[DEBUG] Output directory:", config.outputDir);
}

// Ensure output directory exists
if (!fs.existsSync(config.outputDir)) {
fs.mkdirSync(config.outputDir, { recursive: true });
if (flags.trace) {
console.log("[INFO] Created output directory:", config.outputDir);
}
}

// Find all MJML files with absolute path
const mjmlFiles = glob.sync(`${config.inputDir}/**/*.mjml`);

console.log(`\n[INFO] Found ${mjmlFiles.length} MJML file(s) to compile...`);

if (mjmlFiles.length === 0) {
console.error("[ERROR] No MJML files found!");
console.error("[ERROR] Looked in:", config.inputDir);
console.error(
"[ERROR] Does this directory exist?",
fs.existsSync(config.inputDir),
);
process.exit(1);
}

// Compile each MJML file
let successCount = 0;
let errorCount = 0;

mjmlFiles.forEach((filePath) => {
try {
const mjmlContent = fs.readFileSync(filePath, "utf8");
const fileName = path.basename(filePath, ".mjml");
const relativePath = path.relative(config.inputDir, filePath);

console.log(`\n[BUILD] Compiling: ${relativePath}`);

// Compile MJML to HTML
const result = mjml2html(mjmlContent, {
minify: config.minify,
validationLevel: config.validationLevel,
filePath: filePath, // Important: tells MJML where the file is for resolving includes
mjmlConfigPath: __dirname, // Point to the directory with .mjmlconfig
});

// Check for errors
if (result.errors.length > 0) {
console.error(`[ERROR] Failed to compile ${fileName}.mjml:`);
result.errors.forEach((err) =>
console.error(` ${err.formattedMessage}`),
);
errorCount++;
return;
}

// Calculate output path preserving directory structure
const relativeDir = path.dirname(relativePath);
const outputDir = path.join(config.outputDir, relativeDir);

// Ensure subdirectory exists
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}

const outputExtension = config.hbsOutput ? ".html.hbs" : ".html";
const outputPath = path.join(outputDir, `${fileName}${outputExtension}`);
fs.writeFileSync(outputPath, result.html);

console.log(
`[OK] Built: ${fileName}.mjml โ†’ ${path.relative(__dirname, outputPath)}`,
);
successCount++;

// Log warnings if any
if (result.warnings && result.warnings.length > 0) {
console.warn(`[WARN] Warnings for ${fileName}.mjml:`);
result.warnings.forEach((warn) =>
console.warn(` ${warn.formattedMessage}`),
);
}
} catch (error) {
console.error(`[ERROR] Exception processing ${path.basename(filePath)}:`);
console.error(` ${error.message}`);
errorCount++;
}
});

console.log(`\n[SUMMARY] Compilation complete!`);
console.log(` Success: ${successCount}`);
console.log(` Failed: ${errorCount}`);
console.log(` Output: ${config.outputDir}`);

if (errorCount > 0) {
process.exit(1);
}
4 changes: 0 additions & 4 deletions src/Core/MailTemplates/Mjml/build.sh

This file was deleted.

14 changes: 11 additions & 3 deletions src/Core/MailTemplates/Mjml/components/head.mjml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@
<mj-body background-color="#e6e9ef" width="660px" />
</mj-attributes>
<mj-style inline="inline">
.link { text-decoration: none; color: #175ddc; font-weight: 600 }
.link {
text-decoration: none;
color: #175ddc;
font-weight: 600;
}
</mj-style>
<mj-style>
.border-fix > table { border-collapse:separate !important; } .border-fix >
table > tbody > tr > td { border-radius: 3px; }
.border-fix > table {
border-collapse: separate !important;
}
.border-fix > table > tbody > tr > td {
border-radius: 3px;
}
</mj-style>
4 changes: 3 additions & 1 deletion src/Core/MailTemplates/Mjml/emails/two-factor.mjml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
<mj-section>
<mj-column>
<mj-text>
<p>Your two-step verification code is: <b>{{Token}}</b></p>
<p>
Your two-step verification code is: <b>{{ Token }}</b>
</p>
<p>Use this code to complete logging in with Bitwarden.</p>
</mj-text>
</mj-column>
Expand Down
6 changes: 4 additions & 2 deletions src/Core/MailTemplates/Mjml/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
},
"homepage": "https://bitwarden.com",
"scripts": {
"build": "./build.sh",
"watch": "nodemon --exec ./build.sh --watch ./components --watch ./emails --ext js,mjml",
"build": "node ./build.js",
"build:hbs": "node ./build.js --hbs",
"build:minify": "node ./build.js --hbs --minify",
"build:watch": "nodemon ./build.js --watch emails --watch components --ext mjml,js",
"prettier": "prettier --cache --write ."
},
"dependencies": {
Expand Down
78 changes: 78 additions & 0 deletions src/Core/MailTemplates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
Email templating
================

We use MJML to generate the HTML that our mail services use to send emails to users. To accomplish this, we use different file types depending on which part of the email generation process we're working with.

# File Types

## `*.html.hbs`
These are the compiled HTML email templates that serve as the foundation for all HTML emails sent by the Bitwarden platform. They are generated from MJML source files and enhanced with Handlebars templating capabilities.

### Generation Process
- **Source**: Built from `*.mjml` files in the `./mjml` directory.
- The MJML source acts as a toolkit for developers to generate HTML. It is the developers responsibility to generate the HTML and then ensure it is accessible to `IMailService` implementations.
- **Build Tool**: Generated via node build scripts: `npm run build`.
- The build script definitions can be viewed in the `Mjml/package.json` as well as in `Mjml/build.js`.
- **Output**: Cross-client compatible HTML with embedded CSS for maximum email client support
- **Template Engine**: Enhanced with Handlebars syntax for dynamic content injection

### Handlebars Integration
The templates use Handlebars templating syntax for dynamic content replacement:

```html
<!-- Example Handlebars usage -->
<h1>Welcome {{userName}}!</h1>
<p>Your organization {{organizationName}} has invited you to join.</p>
<a href="{{actionUrl}}">Accept Invitation</a>
```

**Variable Types:**
- **Simple Variables**: `{{userName}}`, `{{email}}`, `{{organizationName}}`

### Email Service Integration
The `IMailService` consumes these templates through the following process:

1. **Template Selection**: Service selects appropriate `.html.hbs` template based on email type
2. **Model Binding**: View model properties are mapped to Handlebars variables
3. **Compilation**: Handlebars engine processes variables and generates final HTML

### Development Guidelines

**Variable Naming:**
- Use camelCase for consistency: `{{userName}}`, `{{organizationName}}`
- Prefix URLs with descriptive names: `{{actionUrl}}`, `{{logoUrl}}`

**Testing Considerations:**
- Verify Handlebars variable replacement with actual view model data
- Ensure graceful degradation when variables are missing or null, if necessary
- Validate HTML structure and accessibility compliance

## `*.txt.hbs`
These files provide plain text versions of emails and are essential for email accessibility and deliverability. They serve several important purposes:

### Purpose and Usage
- **Accessibility**: Screen readers and assistive technologies often work better with plain text versions
- **Email Client Compatibility**: Some email clients prefer or only display plain text versions
- **Fallback Content**: When HTML rendering fails, the plain text version ensures the message is still readable

### Structure
Plain text email templates use the same Handlebars syntax (`{{variable}}`) as HTML templates for dynamic content replacement. They should:

- Contain the core message content without HTML formatting
- Use line breaks and spacing for readability
- Include all important links as full URLs
- Maintain logical content hierarchy using spacing and simple text formatting

### Email Service Integration
The `IMailService` automatically uses both versions when sending emails:
- The HTML version (from `*.html.hbs`) provides rich formatting and styling
- The plain text version (from `*.txt.hbs`) serves as the text alternative
- Email clients can choose which version to display based on user preferences and capabilities

### Development Guidelines
- Always create a corresponding `*.txt.hbs` file for each `*.html.hbs` template
- Keep the content concise but complete - include all essential information from the HTML version
- Test plain text templates to ensure they're readable and convey the same message

## `*.mjml`
This is a templating language we use to increase efficiency when creating email content. See the readme within the `./mjml` directory for more comprehensive information.
Loading