diff --git a/backend/internal/service/email_service.go b/backend/internal/service/email_service.go index 8ecaf7e5..3f0a7e30 100644 --- a/backend/internal/service/email_service.go +++ b/backend/internal/service/email_service.go @@ -282,16 +282,18 @@ func prepareBody[V any](srv *EmailService, template email.Template[V], data *ema var htmlHeader = textproto.MIMEHeader{} htmlHeader.Add("Content-Type", "text/html; charset=UTF-8") - htmlHeader.Add("Content-Transfer-Encoding", "8bit") + htmlHeader.Add("Content-Transfer-Encoding", "quoted-printable") htmlPart, err := mpart.CreatePart(htmlHeader) if err != nil { return "", "", fmt.Errorf("create html part: %w", err) } - err = email.GetTemplate(srv.htmlTemplates, template).ExecuteTemplate(htmlPart, "root", data) + htmlQp := quotedprintable.NewWriter(htmlPart) + err = email.GetTemplate(srv.htmlTemplates, template).ExecuteTemplate(htmlQp, "root", data) if err != nil { return "", "", fmt.Errorf("execute html template: %w", err) } + htmlQp.Close() err = mpart.Close() if err != nil { diff --git a/backend/resources/email-templates/api-key-expiring-soon_html.tmpl b/backend/resources/email-templates/api-key-expiring-soon_html.tmpl index ba102b29..a7ee76b1 100644 --- a/backend/resources/email-templates/api-key-expiring-soon_html.tmpl +++ b/backend/resources/email-templates/api-key-expiring-soon_html.tmpl @@ -1,3 +1 @@ -{{define "root"}}
-{{.AppName}}

{{.AppName}}

API Key Expiring Soon

-

Warning

Hello {{.Data.Name}},
This is a reminder that your API key {{.Data.APIKeyName}} will expire on {{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}.

Please generate a new API key if you need continued access.

{{end}} \ No newline at end of file +{{define "root"}}
{{.AppName}}

{{.AppName}}

API Key Expiring Soon

Warning

Hello {{.Data.Name}},
This is a reminder that your API key {{.Data.APIKeyName}} will expire on {{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}.

Please generate a new API key if you need continued access.

{{end}} \ No newline at end of file diff --git a/backend/resources/email-templates/login-with-new-device_html.tmpl b/backend/resources/email-templates/login-with-new-device_html.tmpl index 85d2e4dc..c91a7ea1 100644 --- a/backend/resources/email-templates/login-with-new-device_html.tmpl +++ b/backend/resources/email-templates/login-with-new-device_html.tmpl @@ -1,5 +1 @@ -{{define "root"}}
-{{.AppName}}

{{.AppName}}

New Sign-In Detected

-

Warning

Your {{.AppName}} account was recently accessed from a new IP address or browser. If you recognize this activity, no further action is required.

Details

Approximate Location

-

{{if and .Data.City .Data.Country}}{{.Data.City}}, {{.Data.Country}}{{else if .Data.Country}}{{.Data.Country}}{{else}}Unknown{{end}}

IP Address

{{.Data.IPAddress}}

Device

-

{{.Data.Device}}

Sign-In Time

{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}

{{end}} \ No newline at end of file +{{define "root"}}
{{.AppName}}

{{.AppName}}

New Sign-In Detected

Warning

Your {{.AppName}} account was recently accessed from a new IP address or browser. If you recognize this activity, no further action is required.

Details

Approximate Location

{{if and .Data.City .Data.Country}}{{.Data.City}}, {{.Data.Country}}{{else if .Data.Country}}{{.Data.Country}}{{else}}Unknown{{end}}

IP Address

{{.Data.IPAddress}}

Device

{{.Data.Device}}

Sign-In Time

{{.Data.DateTime.Format "January 2, 2006 at 3:04 PM MST"}}

{{end}} \ No newline at end of file diff --git a/backend/resources/email-templates/one-time-access_html.tmpl b/backend/resources/email-templates/one-time-access_html.tmpl index ba0ae8c4..6b6fb0b6 100644 --- a/backend/resources/email-templates/one-time-access_html.tmpl +++ b/backend/resources/email-templates/one-time-access_html.tmpl @@ -1,4 +1 @@ -{{define "root"}}
-{{.AppName}}

{{.AppName}}

Your Login Code

Click the button below to sign in to -{{.AppName}} with a login code.
Or visit {{.Data.LoginLink}} and enter the code {{.Data.Code}}.

This code expires in {{.Data.ExpirationString}}.

{{end}} \ No newline at end of file +{{define "root"}}
{{.AppName}}

{{.AppName}}

Your Login Code

Click the button below to sign in to {{.AppName}} with a login code.
Or visit {{.Data.LoginLink}} and enter the code {{.Data.Code}}.

This code expires in {{.Data.ExpirationString}}.

{{end}} \ No newline at end of file diff --git a/backend/resources/email-templates/test_html.tmpl b/backend/resources/email-templates/test_html.tmpl index 34a720d0..21a4bdde 100644 --- a/backend/resources/email-templates/test_html.tmpl +++ b/backend/resources/email-templates/test_html.tmpl @@ -1,3 +1 @@ -{{define "root"}} -
-{{.AppName}}

{{.AppName}}

Test Email

Your email setup is working correctly!

{{end}} \ No newline at end of file +{{define "root"}}
{{.AppName}}

{{.AppName}}

Test Email

Your email setup is working correctly!

{{end}} \ No newline at end of file diff --git a/email-templates/build.ts b/email-templates/build.ts index c8b86e76..3c235918 100644 --- a/email-templates/build.ts +++ b/email-templates/build.ts @@ -12,40 +12,6 @@ function getTemplateName(filename: string): string { return filename.replace(".tsx", ""); } -/** - * Tag-aware wrapping: - * - Prefer breaking immediately after the last '>' within maxLen. - * - Never break at spaces. - * - If no '>' exists in the window, hard-break at maxLen. - */ -function tagAwareWrap(input: string, maxLen: number): string { - const out: string[] = []; - - for (const originalLine of input.split(/\r?\n/)) { - let line = originalLine; - while (line.length > maxLen) { - let breakPos = line.lastIndexOf(">", maxLen); - - // If '>' happens to be exactly at maxLen, break after it - if (breakPos === maxLen) breakPos = maxLen; - - // If we found a '>' before the limit, break right after it - if (breakPos > -1 && breakPos < maxLen) { - out.push(line.slice(0, breakPos + 1)); - line = line.slice(breakPos + 1); - continue; - } - - // No suitable tag end found—hard break - out.push(line.slice(0, maxLen)); - line = line.slice(maxLen); - } - out.push(line); - } - - return out.join("\n"); -} - async function buildTemplateFile( Component: any, templateName: string, @@ -58,11 +24,7 @@ async function buildTemplateFile( // Normalize quotes const normalized = rendered.replace(/"/g, '"'); - // Enforce line length: prefer tag boundaries, never spaces - const maxLen = isPlainText ? 78 : 998; // RFC-safe - const safe = tagAwareWrap(normalized, maxLen); - - const goTemplate = `{{define "root"}}${safe}{{end}}`; + const goTemplate = `{{define "root"}}${normalized}{{end}}`; const suffix = isPlainText ? "_text.tmpl" : "_html.tmpl"; const templatePath = path.join(outputDir, `${templateName}${suffix}`); @@ -98,7 +60,7 @@ async function discoverAndBuildTemplates() { } await buildTemplateFile(Component, templateName, false); // HTML - await buildTemplateFile(Component, templateName, true); // Text + await buildTemplateFile(Component, templateName, true); // Text console.log(`✓ Built ${templateName}`); } catch (error) { @@ -112,4 +74,4 @@ async function main() { console.log("All templates built successfully!"); } -main().catch(console.error); \ No newline at end of file +main().catch(console.error);