mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 15:33:17 +00:00
Agregando diseño de correo
This commit is contained in:
parent
bce7e0baf6
commit
6a0c6a4a49
13 changed files with 297 additions and 15 deletions
|
|
@ -15,6 +15,7 @@ WORKDIR /app
|
||||||
|
|
||||||
COPY db/ ./db/
|
COPY db/ ./db/
|
||||||
COPY api/ ./api/
|
COPY api/ ./api/
|
||||||
|
COPY web-panel/public/brand/panel-icon-symmetric.png ./api/assets/email/logo.png
|
||||||
|
|
||||||
RUN chmod +x ./db/bootstrap-tools.sh \
|
RUN chmod +x ./db/bootstrap-tools.sh \
|
||||||
&& ./db/bootstrap-tools.sh \
|
&& ./db/bootstrap-tools.sh \
|
||||||
|
|
|
||||||
BIN
api/assets/email/icon-lock.png
Normal file
BIN
api/assets/email/icon-lock.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 280 B |
BIN
api/assets/email/icon-shield.png
Normal file
BIN
api/assets/email/icon-shield.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 369 B |
BIN
api/assets/email/icon-user.png
Normal file
BIN
api/assets/email/icon-user.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 380 B |
BIN
api/assets/email/logo-footer.png
Normal file
BIN
api/assets/email/logo-footer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
api/assets/email/logo.png
Normal file
BIN
api/assets/email/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
BIN
api/assets/email/petal.png
Normal file
BIN
api/assets/email/petal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 656 B |
|
|
@ -21,11 +21,12 @@ export const config = {
|
||||||
seedPassword: Deno.env.get("SEED_PASSWORD") ?? "",
|
seedPassword: Deno.env.get("SEED_PASSWORD") ?? "",
|
||||||
vcardBase: Deno.env.get("VCARD_BASE") ?? "https://vcard.arctec.com.mx?info=",
|
vcardBase: Deno.env.get("VCARD_BASE") ?? "https://vcard.arctec.com.mx?info=",
|
||||||
panelLoginUrl: Deno.env.get("PANEL_LOGIN_URL") ?? "http://localhost:3000/login",
|
panelLoginUrl: Deno.env.get("PANEL_LOGIN_URL") ?? "http://localhost:3000/login",
|
||||||
smtpHost: Deno.env.get("SMTP_HOST") ?? "",
|
smtpHost: (Deno.env.get("SMTP_HOST") ?? "").trim(),
|
||||||
smtpPort: Number(Deno.env.get("SMTP_PORT") ?? "587"),
|
smtpPort: Number(Deno.env.get("SMTP_PORT") ?? "587"),
|
||||||
smtpUser: Deno.env.get("SMTP_USER") ?? "",
|
smtpUser: (Deno.env.get("SMTP_USER") ?? "").trim(),
|
||||||
smtpPass: Deno.env.get("SMTP_PASS") ?? "",
|
smtpPass: Deno.env.get("SMTP_PASS") ?? "",
|
||||||
smtpFrom: Deno.env.get("SMTP_FROM") ?? "PANELS <noreply@panel.local>",
|
/** Compose puede pasar SMTP_FROM vacío; no pisar el default. */
|
||||||
|
smtpFrom: (Deno.env.get("SMTP_FROM") ?? "").trim() || "PANELS <noreply@panel.local>",
|
||||||
/** En HTTPS (Coolify) poner COOKIE_SECURE=true */
|
/** En HTTPS (Coolify) poner COOKIE_SECURE=true */
|
||||||
cookieSecure: ["1", "true", "yes"].includes(
|
cookieSecure: ["1", "true", "yes"].includes(
|
||||||
(Deno.env.get("COOKIE_SECURE") ?? "").toLowerCase(),
|
(Deno.env.get("COOKIE_SECURE") ?? "").toLowerCase(),
|
||||||
|
|
|
||||||
9
api/email_assets.ts
Normal file
9
api/email_assets.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/** Assets de correo: logo desde web-panel/public/brand/panel-icon-symmetric.png */
|
||||||
|
export const emailAssets = {
|
||||||
|
logo: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAANs0lEQVR4nO2df4wd1XXHP+fembfvvf1he9cxGNsQKMVmARtighLF7UKLIaYhalXWiRApFaWkEWBapb+oFC1bKSqqVEwcRJMglTYiUmIrimLxo6Y1xlXTOlCgi4X54QQC2BhsL95fb9+vuff0j3lv11683vdm1q6B/Urvab07M2fu95575sw55x4LqaBCH9IDBmDswHOS7nqnBm2LVyvATvD0oyB6Sm+gt1dt72a1p1ToSURvr9re3mTjaUpjenvVbtmCr8/Y6ts0jPJjF2D011G/VNR3qeppr4UickQk2IeTvR0L2l7b2S+l+t/iMYpr+FoNHaUq3IPQLx5g1V2jV2JYD+5q1J9vwzbBBE0P5P8V3uGqBQR5Q4x5ynvZPHB/25MA9KmBe6C/3890mZkJ7N1s2bLeAVz6Z8PXKeavjLG/KUELvlpEXRlFHcqptSNpIYggVmwGE+RRX0V99RlR/fsXNnb8GBrTxhMS2LtZ7Zb14lb+yS8WmdyiTRK0fAlVfGXMK3gBg4jMdJ3TGopX1IMam2kzYgJ8tfRoxY3cvmfTkrd6+nYEO/uviqY7fdqB9/RpsLNfolV3HLhCMu0/NGH+3Kj0votPko/MA+QYqPcKGmQXWB+V33XR6E27N525vc7F8U4xx/tl72a1O/slWnn7/msl075DxJwbFQcjQexHljwAMUbE2Kh0JAJ/pg3a/3XVnQfX7+yXqKdvx3GN/Ac1sE8N/eIvvv3Q5UFLfifq8t6VnYj56BJ3HKh6LyYUMYG6aOya3ZvO3H48m3gsgX1qAD59eGRBJcPzxoRnu2rhY0deHaremyArKO9bV/3Uc5sWvE3fpDcCU5Zw7574j2Vb+Y7NtJ/tqoXo40oegIgxPip6E+a6IvzDINo35ZgJAns3x+p56YbDa4OWeTdExSORyIfNuZt9iFgblYYim1vwWys3DH65v1/80fZwcgn3qeEedOVdh39mMx2fcdVR3+gDQwRMSkdGAT+j2xrDmPR+k1fQRj1XVW/CvLhq8ZVowYFL99xzURWJ38YCqDmM/eJWHnl/jc20ftZXRr1IY+RZA+UqjJdrdyPQlEst8UACA205QWqDO+6hNdbGikrV1f7dpKz68bkWIRuCa2TSRIyLCi7Izr/QDsnnEdla9w8DgIPdT9duzd8ktkWV8dhJngFGYKigLFto+O1VLaxYEpAJG9cOrX0NjirPvFblP/ZUASWbkQ9ooxGoRBA5ZU13yGeXhyyaZyZIbVReNYK9BxxPDVR4/T3HvLxMO2HHnmwURD36FWDroouuVIjnX0B09W0aRtnDr5ogd66PSh6RExJoBEaLyvo1Wb7+uzk622bke0bserXKN35QYN+gI5eZHJgRKFWVrnbD397YylWXZFLLGi0qDzxW5OHtRdqyjZCoKiYj3pUHsyrnPfPthSOgYuiLFcbnRs9HgnO8KzMTedbA8HhM3jdvaqWzzeA8qT+fWR7y8IYOFrSZiSUqQOQhlxEeuqODqy7JzIqs9pxw9w15/viaHMPjip1x/kXUVdQG+a5KGF4M0NuLMfVgqDduhc20GVU98cuzxDZvaZfhr38/j9fY+FuT/lN1sHSh4a7rcxRKipH4gTFaVG67NseKJZaqmx1Z9fv+0+vzLF9iKVZ0RnOg4CTMoq58EcDBbiY1Tb1bRuzynVCZjUChrFx7WYb2nKAaD3I2EJj4gbLuUxnO6jRUolhbOtuEL346gyoNaEpjMBIPNAzgC5e3UKw06kkIYJdBHIE39TC8IJ2NChdgxdJg1uNXdQ3oyBuWLbRETokcLO60nDHfzoq7NBUKLF9qsdL4A12VTohTA4nnM7QnN4YV2lgbVSE0s6flUyFAaKWpp/nRSHxbJzt6qtP8fLJlNYuTNK8fH8wRmBJzBKbEHIEpMUdgSswRmBJzBKbEHIEpMUdgSswRmBKJCUz67pjk+qdSVrNIRKACR8a08aRME5BajmRkXDFmMh5YquiE7NmEKgyNeZxPFhxpmkAljtv99yvVWdcM7+MBvf6u4/V3HZlACC3sG3S8vC9CtfHMXSNQ4gn7rxRjaZpA7+Ps2dMvVXj+l1EcSY6aSBFOd12NPyLwvSeLFMpxmF0EnIPvbitNDDItiapx9DswcYLpiecqtOeksQzdFCRawnE9m/CX/zLGm4ccYTBpR+oxvGY+EAdKAwvf31HiJ7vKzMvLZP4iL/z7QIUHHi9ij4oNJpFV17rQwsEhz9f/aYxypIkDtYkqD7xCNgP7Bx03/sMIt1+X49rLMnS1N5dmrCNysPdAxD9vj8nLtxybJfO1JND9W8d5bX/ErWtzXLg0IExYNzFUUJ56scIDjxXZ/76jLZtM+yAhgRAPKpcRhseVb/ygwIOPF1m60BDa5hkcHld+ddBRKOm0eVpV6MgLjz9X4akXq3xykWFBglRq5OGdQcf+QU9LRmhNQR6kIBBiTQwttLTGRA6+ESWyhcZAJhDmt554MN4TE+zhjfc8vzjQcC34BEQgsMK8VpnIzKXBrBUPWRPXoiclsFEbVLdhsR1MYC9kdvMrqQg0Ei+J4aLS2S4s7TIECZbwaFF5531H5OIHxnRaYWs+oQic1WmZl29elvPKe0OewyNKazZ2kxoq7ZgGiQmsl1vkM8Jf/F6e37k8w+JOS2AAaUw36tpaKCt73op4eHuJfxuoTOSbp8obLsR1MbeuzbHykwHtOWlcVu3LeXhvyLPthQoPPVlkqODJtUw/aTMhEYFSK/TpbDN87/Z2upclm4f6E7stK1xxQcgVF4Q88HiRb20dP0YT66Ukf7Q2x9035JPJqn0FFpZ0GW65OstVl4Tc9uAo+wc92TCZJib2A6tOuffmNrqXBROOdNKVoEzWrNxxXY51qzOM1OpV6kVMay4MufuGuJTE+eSOu1JzpCM49wzLfbe0IZJ8GTdNoDEwWooHtObCEOeZcKSTvtkJkyUbqvC1dTlawpo7U7vo19blJv5ef0NJKkskvufIwyXnBFx7WYbRYiMFRh9E06cIseO7pjsz6y/2dWKWnxVw3hmWclWpRrC0y3LJObGZmM0nqBBPyG90h4k1OpkNBBZ2yEkp7agXKy1oi22gAPNahXyLTMieTYhAV4eJ3bAE5ycv7TjJ9RZHX/9UymoWcxHplJgjMCXmCEyJOQJTYo7AlJgjMCXmCEyJOQJTYo7AlJgjMCXmCEyJxAQ6f3K3HxydXPI+Xdi9EVlJ34dNvREX6FCjJymw9x0365GR+hgKJWX/oCMMhCCAd4c8h4d9XNpxEoj85YEIp41HegwMQW2rV/2XXtiH1trCnABeIZ8RnvzfSrxBj9kbVFTbobn9xQr7Bj2ZIC6/ODTieeKFSlzmMUu1MXWN8x4e+58K2bBxLVThbaht9boSPEBGzKu+MqYyw1ZXrVUlvPGe4/6txYmUZJRy+2nk4hzz4RHPxq3FeNN1TePassI/PlHkrUOO0MbHpt3uCnEA97vbiux+M/pANcTxIKjVqIRBXgZYtAed2HB9/p3a0iqH90qQW6ZR2SMnto9GYKyk3Lo2x51fyJHLpF/QL70d8TffL/DK/ojW7GRSyQgUK8qyhZa/+4M2Vv9a+nR21cFD24psenScXEYasOeqYkLxrjKk+fC83ffOPwIaZxbq+/9Xbjj0cJDtvDkqHXEiMuNdGomzZcuXWNatbmHFEksmqBHZcK4RBkc9u16r8uQLFUpVpbXlgxUKxkCpohgjXL0qw+dWxFv+m5VVdcreA45tz1fY/WZERz7epjkTgapEQUuHdeXhRwc2LfxivSlbALBoz5X1tOkjqtU/BG2oL4ZXmN8q/Oqg476fjsc5jQbGMhV1A96WE/KZ45d3eA/ZWqLp0WfLbP15OVESqJ4BzIbMWEoy5Q4EEcHIIwA93Z+QnUxpe9K7B3lt8eCzJtN6qasWErU9SfI8qd9Eo4OZyOClkNXUE13VS5gVrZbeyNnxi3dtXFZr2CiT+4V7eNps2SLOq+kXE0oz/QBVJ42zT/A52rA3gtmQ1YznoKg3Qauo8M1dG88u9vRh6108j1lx9eZaqzYcesJmF3w+Ks11L1L1zrbMs748+vOBA52f6+tG+6frnbWlG0VVsHKrqxYOmSAbqM5mVfKHDKpebEY0Khe8+JvZIq5/yiHHmuF+8azHDGxcuJ9o/EuIcWJC+ViSqOoxVo3NGnWFr+z+1ide7e1Ve3TnNjjeu/AWcT19O4KBby/eoZWRG8WG3gRZo6rTtsH8qEHVO7GhMUHO+srIVwc2nfmTnj4NjtdPdVqvo9728tIN764laH/EBJlFUWk4krhv6kcziqPqFbxt6QjUV0d8deyWFzed8eMTtQA9odtWP3HlHQfONZnW75ggf426Cq46Xn9prjeh/RBDVcGjigly1oR5fKXwMy2PfXXgwcUvnYg8aMDvPbrt5aoNwzcRmD+3NrsKAV8dR11FlcYbV59OEDDxUs2DCL5afFXV3Tdw//yHAE3dBnkCtd6CiGhPnwZDI6PXC3xZ8T3GhGeIzZ78DW2zDkWjMt5VDovY/xTDj1r8kZ/u2nh2EZjoJTvTVZoa9dQZWXXXkfmIvUiEFYouUx91oadzT+naVijliIjdZwLzchDqS8/eO2+wfkSzreCT3ISkad5/WqJPTTye5v8fgHTaoiq96zEHu09nrZsei/agWzbj6+08k+D/AIhwAMeXWaaoAAAAAElFTkSuQmCC",
|
||||||
|
logo_footer: "data:image/png;base64,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",
|
||||||
|
petal: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAAA4CAYAAACohjseAAACV0lEQVR4nO3XP2sUQRyH8ec7e3sxGAsFY5HGQrAJioJiI+TAVksLLay09xUkFmrhO8kRsEqbKxQtLGyu0VJUkqjgn2D+7M7X4iIEwh0Xk5gbmE957C2/Z2duloMsy46SDupGtjXXoZgBVmdwt9M5sHtnh8V2WFz02OI7jx31LP009voF22q3CQCSamAD4PmL1RNVefySCjVi9HoRetcctT0HCpjtYphjdmmp8ajVqgCqYvy6xFNqTwRpOdajsapDHwS21e5S3p7W5t/PFt78uhyr4q5kIU9i3UAqMWvGe354h2GoIebnXWxvx82Fl58nY3lqKsStGOvilsR9gRz1Efhg24hC4MMdfThDBZ6+Qmk7SrKLEzcVq4cm/JbYwv5kALkGjQWwD/D1s18DA20LQNK67bDweu1BDOEO9pQggr8AP3tXqwGEUYqDAcNsxwlw+9X3k6E8do0YnyFNOsb3QqVFKUbjtOyn73Bz7W4pKUqywrF7xPgEGBOsKIRxRHPU42DQgGvHg20tLbkhcQ48jVi3+YHdYMS2Yj99A0+d2bIkt1qqcFwBLdsUQJMROSGH0Tfw23K5c4U2scv/MM+B23WK2pYkz5w9W114+3NSG7pao4vCvyQi0SGNzdmzawXnOp0CoNVSFarivAmPkWckfcXUiOL/j/nvBp6CNWqAJ7Cbjun87nYa+KIvojdqtIJoQlor99fAFXTvL0+T3umZ5AqO/It6v3Jg6nJg6nJg6nJg6nJg6nJg6nJg6nJg6nJg6nJg6nJg6nJg6nJg6nJg6nJg6nJg6v4ARcjZVhf1qQsAAAAASUVORK5CYII=",
|
||||||
|
icon_user: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABQ0lEQVR4nO1Z2w0CIRBEYwcWYmnWYmkWYg36YUgI2YV9zKIcTOKPBzO77IM7SGljY22c0IS3++vdev58XKGaELKe0RwQzpgJOKN7RlnncTBNqo2wiiN41BNKUVQ+ezjFgyMMR2ico4gtKLmljUHkACUQBXgK5ZXQEnsLVKorankaceRGJtFnU8iyOdW1Uv5QvKpJmonS8WheVRFPg6jVjxg/fQSO54C170ci20KlESQCLYEa6AU6XgpZUUaBikT5PzI9LyiilL6GZSPRX14coA6kxNdDVFOAO5AxqotNX8QktFt9NParxJT4lzQ6/PdA14FfRkGizTrA9fGo1GrxuvYUihjtRLhGpICXW1XE1BGj1RHu7VTL5T7c1b60tcZbzmAhx+veG5ohx+slpKtIwRM9CmteMdWY8pKPw+hr1o2N1fEB0r9JGMUsFy4AAAAASUVORK5CYII=",
|
||||||
|
icon_lock: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAA30lEQVR4nO2Y3Q2DMAyE3aobdBBGYxZG6yDMQF8t5IgkviNKe98jEPsuzi9mQggh/pkHI+iy7kf0/LO94fmgAUvCzyCNwAJ58SWBNd+08kQEqRXm39VW64q0gdZeRZuAVMCsbUgg5wDMwJQs635khkG2vdkPVGB6A2I0qeUMtRllltWuhijhZ3qMvO5OGMHqkGIyRsLeuNMvozIwGqoB1nzx0Ax44UwTFAORYJYJzYGIaINj/FIxI1bAC2aJN9MQGo8MjCZ1nL6COXlToI8ImXipCtx6ESkw/Z1YCCFEii9K4GqxBXxqhgAAAABJRU5ErkJggg==",
|
||||||
|
icon_shield: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABOElEQVR4nO1Z2w3CMAw0iA0YhNGYpaMxCDPAV6UqivzInZOCfJ9t4tw5bn1pRQqFQkHD4/n+ZMa/ZAXuEX9td/p61IAt6SNh7R4CSpAjOQ+x6HgN0GSUCEPI0CRmBtF47sFZNYyuoQ6YQRpd++YJNpN4u6bVR65z6OShBKzGcgGoV1oqYCePiFgmgNUMlwhgdvKlJcToL9MFsA84UwWwTaCIU4CVNU9WR8h74qoCtBNVe11bDCWvzTF3wBJh3c8kL+IsIVSEh0hvvmeO+yG2gvVEIG8cr+Cht5Any1kPbYuQgJFSySqdHeEdiIjIJi8CfFZhNSU0znAn9uyEBUYSICuBiDiNnR4RcTo7HRHBNnQ0NxolM9WNRmEZPyaoArRSyjgLiCTsgOWJ2J8pf/4XUyoYrrRQKPw5vti4AQNfqUCeAAAAAElFTkSuQmCC",
|
||||||
|
} as const;
|
||||||
218
api/mail.ts
218
api/mail.ts
|
|
@ -1,11 +1,21 @@
|
||||||
|
import { join } from "node:path";
|
||||||
import type { PlatformDb } from "./platform_db.ts";
|
import type { PlatformDb } from "./platform_db.ts";
|
||||||
import { config } from "./config.ts";
|
import { config, ROOT } from "./config.ts";
|
||||||
import { resolveSmtp, smtpConfigured as smtpConfiguredFromDb } from "./smtp.ts";
|
import { resolveSmtp, smtpConfigured as smtpConfiguredFromDb } from "./smtp.ts";
|
||||||
|
|
||||||
|
export type MailAttachment = {
|
||||||
|
filename: string;
|
||||||
|
content: Uint8Array;
|
||||||
|
cid: string;
|
||||||
|
contentType?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type MailPayload = {
|
export type MailPayload = {
|
||||||
to: string;
|
to: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
html?: string;
|
||||||
|
attachments?: MailAttachment[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function smtpConfigured(platformDb: PlatformDb): boolean {
|
export function smtpConfigured(platformDb: PlatformDb): boolean {
|
||||||
|
|
@ -22,6 +32,14 @@ export async function sendMail(
|
||||||
}
|
}
|
||||||
|
|
||||||
const s = resolveSmtp(platformDb);
|
const s = resolveSmtp(platformDb);
|
||||||
|
const from = (s.from_address ?? "").trim();
|
||||||
|
if (!from || !/(?:^|<)[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+(?:>|$)/.test(from)) {
|
||||||
|
return {
|
||||||
|
sent: false,
|
||||||
|
error:
|
||||||
|
"Remitente (From) inválido o vacío. Debe ser un correo, ej. PANELS <noreply@tudominio.com>",
|
||||||
|
};
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const nodemailer = await import("npm:nodemailer@6.9.16");
|
const nodemailer = await import("npm:nodemailer@6.9.16");
|
||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
|
|
@ -34,16 +52,23 @@ export async function sendMail(
|
||||||
greetingTimeout: 20000,
|
greetingTimeout: 20000,
|
||||||
socketTimeout: 30000,
|
socketTimeout: 30000,
|
||||||
tls: {
|
tls: {
|
||||||
// Algunos hosting (cPanel) usan certs que fallan la validación estricta en Deno
|
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: false,
|
||||||
servername: s.host,
|
servername: s.host,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const info = await transporter.sendMail({
|
const info = await transporter.sendMail({
|
||||||
from: s.from_address,
|
from,
|
||||||
to: payload.to,
|
to: payload.to,
|
||||||
subject: payload.subject,
|
subject: payload.subject,
|
||||||
text: payload.text,
|
text: payload.text,
|
||||||
|
html: payload.html,
|
||||||
|
attachments: payload.attachments?.map((a) => ({
|
||||||
|
filename: a.filename,
|
||||||
|
content: a.content,
|
||||||
|
cid: a.cid,
|
||||||
|
contentType: a.contentType ?? "image/png",
|
||||||
|
contentDisposition: "inline" as const,
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
const messageId = typeof info?.messageId === "string" ? info.messageId : undefined;
|
const messageId = typeof info?.messageId === "string" ? info.messageId : undefined;
|
||||||
console.log(`[smtp] sent to=${payload.to} id=${messageId ?? "—"} host=${s.host}:${s.port}`);
|
console.log(`[smtp] sent to=${payload.to} id=${messageId ?? "—"} host=${s.host}:${s.port}`);
|
||||||
|
|
@ -77,24 +102,197 @@ Destinatario: ${dest}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function accessEmailBody(opts: {
|
function escapeHtml(value: string): string {
|
||||||
|
return value
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AccessEmailOpts = {
|
||||||
companyName: string;
|
companyName: string;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
contactName?: string;
|
contactName?: string;
|
||||||
}): string {
|
};
|
||||||
const hi = opts.contactName ? `Hola ${opts.contactName},` : "Hola,";
|
|
||||||
|
const LOGO_CID = "panels-logo@panels";
|
||||||
|
|
||||||
|
export function accessEmailAttachments(): MailAttachment[] {
|
||||||
|
const path = join(ROOT, "api", "assets", "email", "logo.png");
|
||||||
|
try {
|
||||||
|
const content = Deno.readFileSync(path);
|
||||||
|
return [{
|
||||||
|
filename: "logo.png",
|
||||||
|
content,
|
||||||
|
cid: LOGO_CID,
|
||||||
|
contentType: "image/png",
|
||||||
|
}];
|
||||||
|
} catch {
|
||||||
|
console.warn("[mail] logo.png no encontrado en", path);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function accessEmailBody(opts: AccessEmailOpts): string {
|
||||||
|
const hi = opts.contactName ? `Hola, ${opts.contactName}` : "Hola";
|
||||||
return `${hi}
|
return `${hi}
|
||||||
|
|
||||||
Su empresa "${opts.companyName}" ya tiene acceso a PANELS.
|
Tu acceso a PANELS está listo.
|
||||||
|
|
||||||
Usuario (administrador): ${opts.username}
|
La empresa ${opts.companyName} ya tiene acceso a PANELS. Utiliza los siguientes datos para iniciar sesión:
|
||||||
|
|
||||||
|
Usuario administrador: ${opts.username}
|
||||||
Contraseña temporal: ${opts.password}
|
Contraseña temporal: ${opts.password}
|
||||||
|
|
||||||
Inicie sesión en: ${config.panelLoginUrl}
|
Ingresar: ${config.panelLoginUrl}
|
||||||
|
|
||||||
Por seguridad, al entrar por primera vez deberá cambiar la contraseña.
|
Por seguridad, al ingresar por primera vez deberás crear una nueva contraseña.
|
||||||
|
|
||||||
— Equipo PANELS
|
— Equipo PANELS
|
||||||
|
Todas tus operaciones. Un solo sistema.
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** HTML del correo de acceso (diseño original + logo real por CID). */
|
||||||
|
export function accessEmailHtml(opts: AccessEmailOpts): string {
|
||||||
|
const name = (opts.contactName ?? "").trim();
|
||||||
|
const hi = name ? `Hola, ${escapeHtml(name)}` : "Hola";
|
||||||
|
const company = escapeHtml(opts.companyName);
|
||||||
|
const username = escapeHtml(opts.username);
|
||||||
|
const password = escapeHtml(opts.password);
|
||||||
|
const loginUrl = escapeHtml(config.panelLoginUrl);
|
||||||
|
|
||||||
|
const mark =
|
||||||
|
`<img src="cid:${LOGO_CID}" width="40" height="40" alt="PANELS" style="display:block;border:0;outline:none;" />`;
|
||||||
|
|
||||||
|
const watermark = `
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="120" height="120" style="border-collapse:collapse;opacity:0.22;">
|
||||||
|
<tr>
|
||||||
|
<td width="60" height="60" style="padding:6px;"><div style="width:48px;height:48px;background:#9DBCF5;border-radius:12px;line-height:48px;font-size:0;"> </div></td>
|
||||||
|
<td width="60" height="60" style="padding:6px;"><div style="width:48px;height:48px;background:#9DBCF5;border-radius:12px;line-height:48px;font-size:0;"> </div></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="60" height="60" style="padding:6px;"><div style="width:48px;height:48px;background:#9DBCF5;border-radius:12px;line-height:48px;font-size:0;"> </div></td>
|
||||||
|
<td width="60" height="60" style="padding:6px;"><div style="width:48px;height:48px;background:#9DBCF5;border-radius:12px;line-height:48px;font-size:0;"> </div></td>
|
||||||
|
</tr>
|
||||||
|
</table>`;
|
||||||
|
|
||||||
|
const footerMark = `
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="40" height="40" style="border-collapse:separate;">
|
||||||
|
<tr>
|
||||||
|
<td width="20" height="20" style="padding:2px;"><div style="width:14px;height:14px;background:#5B8DEF;border-radius:3px;line-height:14px;font-size:0;"> </div></td>
|
||||||
|
<td width="20" height="20" style="padding:2px;"><div style="width:14px;height:14px;background:#5B8DEF;border-radius:3px;line-height:14px;font-size:0;"> </div></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="20" height="20" style="padding:2px;"><div style="width:14px;height:14px;background:#5B8DEF;border-radius:3px;line-height:14px;font-size:0;"> </div></td>
|
||||||
|
<td width="20" height="20" style="padding:2px;"><div style="width:14px;height:14px;background:#5B8DEF;border-radius:3px;line-height:14px;font-size:0;"> </div></td>
|
||||||
|
</tr>
|
||||||
|
</table>`;
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Tu acceso a PANELS está listo</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;padding:0;background-color:#f4f6f9;font-family:Arial,Helvetica,sans-serif;-webkit-text-size-adjust:100%;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#f4f6f9;padding:24px 12px;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table role="presentation" width="560" cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:560px;background-color:#ffffff;border-radius:12px;overflow:hidden;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:28px 32px 8px 32px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr>
|
||||||
|
<td valign="middle" width="48">${mark}</td>
|
||||||
|
<td valign="middle" style="padding-left:12px;font-size:22px;font-weight:700;color:#0B1F4D;letter-spacing:0.5px;">PANELS</td>
|
||||||
|
<td align="right" valign="top" width="100" style="overflow:hidden;">${watermark}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px 32px 8px 32px;">
|
||||||
|
<p style="margin:0 0 16px 0;font-size:16px;line-height:1.5;color:#4B5563;">${hi}</p>
|
||||||
|
<h1 style="margin:0 0 12px 0;font-size:26px;line-height:1.25;font-weight:700;color:#0B1F4D;">Tu acceso a PANELS está listo</h1>
|
||||||
|
<p style="margin:0 0 24px 0;font-size:15px;line-height:1.55;color:#4B5563;">La empresa <strong style="color:#0B1F4D;">${company}</strong> ya tiene acceso a PANELS. Utiliza los siguientes datos para iniciar sesión:</p>
|
||||||
|
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#EEF4FC;border-radius:12px;margin:0 0 24px 0;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:16px 18px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr>
|
||||||
|
<td width="28" valign="middle" style="font-size:18px;color:#2F6FED;">👤</td>
|
||||||
|
<td width="160" valign="middle" style="width:160px;font-size:14px;color:#4B5563;padding-left:8px;">Usuario administrador</td>
|
||||||
|
<td width="16" align="center" valign="middle" style="color:#CBD5E1;">|</td>
|
||||||
|
<td valign="middle" style="font-size:15px;font-weight:700;color:#0B1F4D;font-family:Consolas,Monaco,monospace;">${username}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 18px;">
|
||||||
|
<div style="height:1px;background-color:#D6E4F5;line-height:1px;font-size:0;"> </div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:16px 18px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr>
|
||||||
|
<td width="28" valign="middle" style="font-size:18px;color:#2F6FED;">🔒</td>
|
||||||
|
<td width="160" valign="middle" style="width:160px;font-size:14px;color:#4B5563;padding-left:8px;">Contraseña temporal</td>
|
||||||
|
<td width="16" align="center" valign="middle" style="color:#CBD5E1;">|</td>
|
||||||
|
<td valign="middle" style="font-size:15px;font-weight:700;color:#0B1F4D;font-family:Consolas,Monaco,monospace;">${password}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 12px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="background-color:#2F6FED;border-radius:10px;">
|
||||||
|
<a href="${loginUrl}" style="display:block;padding:14px 24px;font-size:16px;font-weight:700;color:#ffffff;text-decoration:none;">Ingresar a PANELS</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p style="margin:0 0 28px 0;text-align:center;">
|
||||||
|
<a href="${loginUrl}" style="font-size:13px;color:#2F6FED;text-decoration:underline;word-break:break-all;">${loginUrl}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 28px 0;">
|
||||||
|
<tr>
|
||||||
|
<td width="28" valign="top" style="font-size:16px;color:#2F6FED;padding-top:1px;">🛡</td>
|
||||||
|
<td valign="top" style="font-size:13px;line-height:1.5;color:#4B5563;padding-left:8px;">Por seguridad, al ingresar por primera vez deberás crear una nueva contraseña.</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="background-color:#0B1F4D;padding:22px 32px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr>
|
||||||
|
<td valign="middle" style="color:#ffffff;">
|
||||||
|
<div style="font-size:14px;font-weight:700;margin-bottom:4px;">— Equipo PANELS</div>
|
||||||
|
<div style="font-size:12px;opacity:0.85;">Todas tus operaciones. Un solo sistema.</div>
|
||||||
|
</td>
|
||||||
|
<td width="56" align="right" valign="middle">${footerMark}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin:16px 0 0 0;font-size:11px;line-height:1.4;color:#9CA3AF;text-align:center;">Este correo fue enviado automáticamente. No respondas a este mensaje.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
|
||||||
11
api/saas.ts
11
api/saas.ts
|
|
@ -9,7 +9,7 @@ import {
|
||||||
type PlatformDb,
|
type PlatformDb,
|
||||||
} from "./platform_db.ts";
|
} from "./platform_db.ts";
|
||||||
import { companyByCode, normCompanyCode, normalizeCompanyProfile, validateCompanyProfile, type CompanyProfileInput } from "./companies.ts";
|
import { companyByCode, normCompanyCode, normalizeCompanyProfile, validateCompanyProfile, type CompanyProfileInput } from "./companies.ts";
|
||||||
import { accessEmailBody, sendMail } from "./mail.ts";
|
import { accessEmailAttachments, accessEmailBody, accessEmailHtml, sendMail } from "./mail.ts";
|
||||||
import { config } from "./config.ts";
|
import { config } from "./config.ts";
|
||||||
|
|
||||||
export type CreateTenantInput = CompanyProfileInput & {
|
export type CreateTenantInput = CompanyProfileInput & {
|
||||||
|
|
@ -549,13 +549,20 @@ async function sendAccessEmail(
|
||||||
): Promise<{ sent: boolean; error?: string }> {
|
): Promise<{ sent: boolean; error?: string }> {
|
||||||
const result = await sendMail(platformDb, {
|
const result = await sendMail(platformDb, {
|
||||||
to: opts.contactEmail,
|
to: opts.contactEmail,
|
||||||
subject: `Acceso a PANELS — ${opts.companyName}`,
|
subject: `Tu acceso a PANELS está listo — ${opts.companyName}`,
|
||||||
text: accessEmailBody({
|
text: accessEmailBody({
|
||||||
companyName: opts.companyName,
|
companyName: opts.companyName,
|
||||||
username: opts.username,
|
username: opts.username,
|
||||||
password: opts.password,
|
password: opts.password,
|
||||||
contactName: opts.contactName,
|
contactName: opts.contactName,
|
||||||
}),
|
}),
|
||||||
|
html: accessEmailHtml({
|
||||||
|
companyName: opts.companyName,
|
||||||
|
username: opts.username,
|
||||||
|
password: opts.password,
|
||||||
|
contactName: opts.contactName,
|
||||||
|
}),
|
||||||
|
attachments: accessEmailAttachments(),
|
||||||
});
|
});
|
||||||
if (result.sent) {
|
if (result.sent) {
|
||||||
platformDb.prepare(
|
platformDb.prepare(
|
||||||
|
|
|
||||||
10
api/smtp.ts
10
api/smtp.ts
|
|
@ -110,6 +110,11 @@ export type SaveSmtpInput = {
|
||||||
keep_password?: boolean;
|
keep_password?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function looksLikeEmailFrom(value: string): boolean {
|
||||||
|
// Acepta "user@dom.com" o "Nombre <user@dom.com>"
|
||||||
|
return /(?:^|<)[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+(?:>|$)/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
export function saveSmtpSettings(
|
export function saveSmtpSettings(
|
||||||
platformDb: PlatformDb,
|
platformDb: PlatformDb,
|
||||||
input: SaveSmtpInput,
|
input: SaveSmtpInput,
|
||||||
|
|
@ -125,6 +130,11 @@ export function saveSmtpSettings(
|
||||||
const enabled = input.enabled !== false;
|
const enabled = input.enabled !== false;
|
||||||
if (enabled && !host) return { error: "Host SMTP obligatorio si está habilitado" };
|
if (enabled && !host) return { error: "Host SMTP obligatorio si está habilitado" };
|
||||||
if (enabled && !fromAddress) return { error: "Remitente (From) obligatorio si está habilitado" };
|
if (enabled && !fromAddress) return { error: "Remitente (From) obligatorio si está habilitado" };
|
||||||
|
if (enabled && fromAddress && !looksLikeEmailFrom(fromAddress)) {
|
||||||
|
return {
|
||||||
|
error: 'Remitente inválido. Use un correo, ej. PANELS <noreply@tudominio.com>',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const current = readRow(platformDb);
|
const current = readRow(platformDb);
|
||||||
let password = input.password ?? "";
|
let password = input.password ?? "";
|
||||||
|
|
|
||||||
56
docs/mockups/panels-access-email.html
Normal file
56
docs/mockups/panels-access-email.html
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="es" xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta name="x-apple-disable-message-reformatting">
|
||||||
|
<title>Acceso a PANELS — {{company_name}}</title>
|
||||||
|
<style>
|
||||||
|
html,body{margin:0!important;padding:0!important;width:100%!important;background:#f7f9fc}
|
||||||
|
table,td{border-collapse:collapse!important;mso-table-lspace:0!important;mso-table-rspace:0!important}
|
||||||
|
.preheader{display:none!important;max-height:0!important;overflow:hidden!important;opacity:0!important;mso-hide:all!important}
|
||||||
|
@media(max-width:680px){.shell{width:100%!important}.pad{padding-left:24px!important;padding-right:24px!important}.title{font-size:30px!important;line-height:36px!important}.label,.value{display:block!important;width:100%!important;box-sizing:border-box!important;text-align:left!important}.label{padding-bottom:4px!important}.value{padding-top:0!important}.petals{display:none!important}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;padding:0;width:100%;min-width:100%;background:#f7f9fc">
|
||||||
|
<div class="preheader">Tu acceso de administrador a PANELS está listo.</div>
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f9fc">
|
||||||
|
<tr><td align="center" style="padding:40px 16px">
|
||||||
|
<!--[if mso]><table role="presentation" width="620"><tr><td><![endif]-->
|
||||||
|
<table class="shell" role="presentation" width="620" cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:620px;background:#fff;border:1px solid #e4e9f2;border-radius:16px;overflow:hidden">
|
||||||
|
<tr><td class="pad" style="padding:30px 40px;border-bottom:1px solid #e4e9f2">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
|
||||||
|
<td valign="middle"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>
|
||||||
|
<td style="padding-right:14px"><table role="presentation" width="48" cellpadding="0" cellspacing="0" border="0" style="width:48px;height:48px;background:#2f6fed;border-radius:12px"><tr><td align="center" style="padding:7px"><table role="presentation" width="34" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr><td width="15" height="15" style="background:#fff;border-radius:5px 5px 12px 5px"> </td><td width="4"></td><td width="15" height="15" style="background:#fff;border-radius:5px 5px 5px 12px"> </td></tr>
|
||||||
|
<tr><td colspan="3" height="4"></td></tr>
|
||||||
|
<tr><td width="15" height="15" style="background:#fff;border-radius:5px 12px 5px 5px"> </td><td width="4"></td><td width="15" height="15" style="background:#fff;border-radius:12px 5px 5px 5px"> </td></tr>
|
||||||
|
</table></td></tr></table></td>
|
||||||
|
<td style="font:800 30px/34px Inter,Segoe UI,Arial,sans-serif;letter-spacing:2px;color:#0f2557">PANELS</td>
|
||||||
|
</tr></table></td>
|
||||||
|
<td class="petals" align="right" width="104"><table role="presentation" width="92" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr><td width="42" height="42" style="background:#eaf1ff;border-radius:12px 12px 32px 12px"></td><td width="8"></td><td width="42" height="42" style="background:#eaf1ff;border-radius:12px 12px 12px 32px"></td></tr>
|
||||||
|
<tr><td colspan="3" height="8"></td></tr>
|
||||||
|
<tr><td width="42" height="42" style="background:#eaf1ff;border-radius:12px 32px 12px 12px"></td><td width="8"></td><td width="42" height="42" style="background:#eaf1ff;border-radius:32px 12px 12px 12px"></td></tr>
|
||||||
|
</table></td>
|
||||||
|
</tr></table></td></tr>
|
||||||
|
<tr><td class="pad" style="padding:38px 40px 34px;font-family:Inter,Segoe UI,Arial,sans-serif;color:#26324a">
|
||||||
|
<p style="margin:0 0 12px;font-size:17px;line-height:26px;color:#4b5872">Hola, {{recipient_name}}</p>
|
||||||
|
<h1 class="title" style="margin:0 0 18px;font-size:36px;line-height:43px;font-weight:800;color:#0f2557">Tu acceso a PANELS está listo</h1>
|
||||||
|
<p style="margin:0 0 26px;font-size:16px;line-height:25px;color:#4b5872">La empresa <strong style="color:#0f2557">{{company_name}}</strong> ya tiene acceso a PANELS. Utiliza los siguientes datos para iniciar sesión:</p>
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#eef4ff;border:1px solid #dce8ff;border-radius:12px;overflow:hidden">
|
||||||
|
<tr><td class="label" width="52%" style="padding:18px 20px;border-bottom:1px solid #d6e3f8;font-size:14px;font-weight:700;color:#4b5872">Usuario administrador</td><td class="value" width="48%" align="right" style="padding:18px 20px;border-bottom:1px solid #d6e3f8;font:700 16px/21px Consolas,monospace;color:#0f2557">{{admin_username}}</td></tr>
|
||||||
|
<tr><td class="label" width="52%" style="padding:18px 20px;font-size:14px;font-weight:700;color:#4b5872">Contraseña temporal</td><td class="value" width="48%" align="right" style="padding:18px 20px;font:700 16px/21px Consolas,monospace;color:#0f2557;word-break:break-all">{{temporary_password}}</td></tr>
|
||||||
|
</table>
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-top:24px"><tr><td align="center" bgcolor="#2F6FED" style="border-radius:8px"><a href="{{login_url}}" target="_blank" style="display:block;padding:15px 24px;border:1px solid #2f6fed;border-radius:8px;font-size:16px;line-height:22px;font-weight:700;color:#fff;text-decoration:none">Ingresar a PANELS</a></td></tr></table>
|
||||||
|
<p style="margin:12px 0 0;text-align:center;font-size:12px;line-height:18px;color:#71809e;word-break:break-all">Si el botón no funciona, abre este enlace:<br><a href="{{login_url}}" target="_blank" style="color:#2f6fed;text-decoration:underline">{{login_url}}</a></p>
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-top:24px"><tr><td width="34" valign="top" style="font:700 22px/24px Arial;color:#2f6fed">✓</td><td style="font-size:14px;line-height:22px;color:#4b5872">Por seguridad, al ingresar por primera vez deberás crear una nueva contraseña.</td></tr></table>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td class="pad" bgcolor="#0F2557" style="padding:24px 40px;background:#0f2557;font-family:Inter,Segoe UI,Arial,sans-serif"><p style="margin:0 0 5px;font-size:15px;font-weight:700;color:#fff">— Equipo PANELS</p><p style="margin:0;font-size:13px;color:#cbd9f6">Todas tus operaciones. Un solo sistema.</p></td></tr>
|
||||||
|
<tr><td class="pad" align="center" style="padding:18px 40px;background:#f9fbff;font:11px/18px Inter,Segoe UI,Arial,sans-serif;color:#8390a8">Este correo fue enviado automáticamente. No respondas a este mensaje.</td></tr>
|
||||||
|
</table>
|
||||||
|
<!--[if mso]></td></tr></table><![endif]-->
|
||||||
|
</td></tr></table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
Loading…
Reference in a new issue