@stacksjs/buddy 0.72.41 → 0.72.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -60,8 +60,8 @@ var {require}=import.meta;import{log,runCommand}from"@stacksjs/cli";import{ExitC
60
60
  Shutting down proxy...`);proxy.stop();process.exit(0)})}catch(error){log.error(`Failed to start proxy: ${getErrorMessage(error)}`);process.exit(ExitCode.FatalError)}});buddy.command("mail:test","Test mail API connection").action(async()=>{try{const{AWSCloudFormationClient}=await import("@stacksjs/ts-cloud"),cfn=new AWSCloudFormationClient(process.env.AWS_REGION||"us-east-1"),stackName=`${(process.env.APP_NAME||"stacks").toLowerCase().replace(/[^a-z0-9-]/g,"-")}-cloud`,mailApiOutput=((await cfn.describeStacks({stackName})).Stacks?.[0]?.Outputs||[]).find((o)=>o.OutputKey==="MailApiUrl");if(!mailApiOutput?.OutputValue){log.error("Mail API not deployed. Run `buddy deploy` first.");process.exit(ExitCode.FatalError)}const apiUrl=mailApiOutput.OutputValue;log.info(`Testing Mail API at ${apiUrl}`);const response=await fetch(`${apiUrl}/mailboxes`,{headers:{Authorization:`Basic ${Buffer.from("test@stacksjs.com:test").toString("base64")}`}});if(response.status===401)log.success("Mail API is responding (auth required)");else if(response.ok){log.success("Mail API is working!");const data=await response.json();console.log("Response:",JSON.stringify(data,null,2))}else log.warn(`Mail API returned status ${response.status}`);process.exit(ExitCode.Success)}catch(error){log.error(`Failed to test API: ${getErrorMessage(error)}`);process.exit(ExitCode.FatalError)}});buddy.command("mail:credentials [email]","Show SMTP credentials for email access").action(async(emailAddress)=>{loadEnvFiles();const domain=process.env.APP_DOMAIN||process.env.MAIL_DOMAIN||"stacksjs.com",mailHost=`mail.${domain}`,email=emailAddress||`chris@${domain}`,localPart=email.includes("@")?email.split("@")[0]:email,username=email,envKey=`MAIL_PASSWORD_${localPart.toUpperCase().replace(/[^A-Z0-9]/g,"_")}`,rawPassword=process.env[envKey],isCiphertext=typeof rawPassword==="string"&&rawPassword.replace(/^["']/,"").startsWith("encrypted:"),password=isCiphertext?void 0:rawPassword;console.log("");console.log("==========================================================");console.log(` EMAIL CREDENTIALS - ${email}`);console.log("==========================================================");console.log("");console.log(` Email: ${email}`);console.log(` Host: ${mailHost}`);console.log(" Port: 587");console.log(" Security: STARTTLS");console.log(` Username: ${username}`);if(password)console.log(` Password: ${password}`);else if(isCiphertext)console.log(" Password: (encrypted - this machine has no private key for .env.production)");else console.log(` Password: (not set - add ${envKey} to .env.production)`);console.log("");console.log("==========================================================");console.log("");process.exit(ExitCode.Success)});buddy.command("mail:logs","Show mail server logs from production").option("-n, --lines <count>","Number of log lines to show",{default:"50"}).option("-f, --follow","Follow log output (poll every 5s)").option("--filter <pattern>","Filter logs by pattern (e.g. AUTH, LOGIN, error)").action(async(options)=>{loadEnvFiles();const directHost=await resolveOperationalMailHost();if(directHost){const lines=sanitizeLineCount(options.lines),filter=options.filter?options.filter.replace(/[^a-zA-Z0-9_.@\s-]/g,""):"",command=`journalctl -u mail --no-pager -n ${lines}${filter?` | grep -iE ${shellQuote(filter)}`:""}`;try{const output=execSync(`ssh -o BatchMode=yes -o ConnectTimeout=10 root@${shellQuote(directHost)} ${shellQuote(command)}`,{encoding:"utf8"});console.log(output);process.exit(ExitCode.Success)}catch(error){log.error(`Failed to fetch logs from ${directHost}: ${getErrorMessage(error)}`);process.exit(ExitCode.FatalError)}}const region=process.env.AWS_REGION||"us-east-1",appName=(process.env.APP_NAME||"stacks").toLowerCase().replace(/[^a-z0-9-]/g,"-");try{const{EC2Client,SSMClient}=await import("@stacksjs/ts-cloud"),ec2=new EC2Client(region),ssm=new SSMClient(region);log.info("Finding mail server...");const instanceId=(await ec2.describeInstances({Filters:[{Name:"tag:Name",Values:[`${appName}-mail-server`]},{Name:"instance-state-name",Values:["running"]}]})).Reservations?.[0]?.Instances?.[0]?.InstanceId;if(!instanceId){log.error("No running mail server found.");log.info(`Looked for instances tagged: ${appName}-mail-server`);process.exit(ExitCode.FatalError)}log.info(`Found instance: ${instanceId}`);const lines=sanitizeLineCount(options.lines),sanitizedFilter=options.filter?options.filter.replace(/[^a-zA-Z0-9_.@\s-]/g,""):"",filterCmd=sanitizedFilter?` | grep -iE '${sanitizedFilter}'`:"",fetchLogs=async()=>{const cmd=`journalctl -u smtp-server -u mail-server --no-pager -n ${lines} --since "10 minutes ago" 2>&1${filterCmd}`,result=await ssm.runShellCommand(instanceId,[cmd],{timeoutSeconds:30,maxWaitMs:30000,pollIntervalMs:2000});if(!result.success){log.error(`Failed to fetch logs: ${result.error||"Unknown error"}`);return!1}if(result.output){const logLines=result.output.trim().split(`
61
61
  `);for(const line of logLines)if(line.includes("error")||line.includes("Error")||line.includes("AUTH failed"))console.log(` \x1B[31m${line}\x1B[0m`);else if(line.includes("warning")||line.includes("Warning"))console.log(` \x1B[33m${line}\x1B[0m`);else if(line.includes("AUTH success")||line.includes("LOGIN completed")||line.includes("authenticated"))console.log(` \x1B[32m${line}\x1B[0m`);else console.log(` ${line}`)}else console.log(" No log entries found.");return!0};console.log("");console.log("==========================================================");console.log(" MAIL SERVER LOGS");console.log("==========================================================");console.log("");await fetchLogs();if(options.follow){console.log("");log.info("Following logs (Ctrl+C to stop)...");let polling=!1;const poll=setInterval(()=>{if(polling)return;polling=!0;console.log("");fetchLogs().catch((e)=>log.error("Log polling failed:",e)).finally(()=>{polling=!1})},5000);await new Promise((resolve)=>{process.on("SIGINT",()=>{clearInterval(poll);resolve()})})}console.log("");process.exit(ExitCode.Success)}catch(error){log.error(`Failed to fetch logs: ${getErrorMessage(error)}`);process.exit(ExitCode.FatalError)}});buddy.command("mail:status","Show mail server status").action(async()=>{loadEnvFiles();const directHost=await resolveOperationalMailHost();if(directHost){const command='systemctl is-active mail; systemctl show mail -p NRestarts -p ActiveEnterTimestamp --value; ss -H -ltn | grep -E "(:(25|143|465|587|993))\\b"; systemctl is-enabled mail-health.timer; systemctl is-active mail-health.timer';try{const output=execSync(`ssh -o BatchMode=yes -o ConnectTimeout=10 root@${shellQuote(directHost)} ${shellQuote(command)}`,{encoding:"utf8"});console.log(output);process.exit(ExitCode.Success)}catch(error){log.error(`Mail server ${directHost} is unhealthy: ${getErrorMessage(error)}`);process.exit(ExitCode.FatalError)}}const region=process.env.AWS_REGION||"us-east-1",appName=(process.env.APP_NAME||"stacks").toLowerCase().replace(/[^a-z0-9-]/g,"-");try{const{EC2Client,SSMClient}=await import("@stacksjs/ts-cloud"),ec2=new EC2Client(region),ssm=new SSMClient(region),instanceId=(await ec2.describeInstances({Filters:[{Name:"tag:Name",Values:[`${appName}-mail-server`]},{Name:"instance-state-name",Values:["running"]}]})).Reservations?.[0]?.Instances?.[0]?.InstanceId;if(!instanceId){log.error("No running mail server found.");log.info(`Looked for instances tagged: ${appName}-mail-server`);process.exit(ExitCode.FatalError)}const result=await ssm.runShellCommand(instanceId,["echo === Service ===","(systemctl status smtp-server --no-pager 2>&1 || systemctl status mail-server --no-pager 2>&1) | head -15","echo === Ports ===",'ss -tlnp 2>&1 | grep -E "(25|587|465|993|143|110|995)" || echo "No mail ports listening"',"echo === Memory ===",`ps aux | grep -E "(smtp-server|bun|mail)" | grep -v grep | awk '{print $6/1024 "MB", $11}'`],{timeoutSeconds:30,maxWaitMs:30000,pollIntervalMs:2000});console.log("");console.log("==========================================================");console.log(" MAIL SERVER STATUS");console.log("==========================================================");console.log("");if(result.success&&result.output)for(const line of result.output.trim().split(`
62
62
  `))console.log(` ${line}`);else log.error(`Failed: ${result.error}`);console.log("");process.exit(ExitCode.Success)}catch(error){log.error(`Failed to get status: ${getErrorMessage(error)}`);process.exit(ExitCode.FatalError)}});buddy.command("mail:port25:request","Request port 25 unblock for direct SMTP delivery").option("--provider <provider>","Cloud provider: aws or hetzner",{default:"aws"}).option("--instance-id <id>","AWS EC2 instance ID (auto-detected if not provided)").option("--server-id <id>","Hetzner server ID (auto-detected if not provided)").option("--elastic-ip <ip>","AWS Elastic IP address").option("--rdns <hostname>","Reverse DNS hostname (e.g. mail.stacksjs.com)").option("--use-case <text>","Description of email use case").action(async(options)=>{loadEnvFiles();const domain=process.env.APP_DOMAIN||process.env.MAIL_DOMAIN||"stacksjs.com",provider=options.provider.toLowerCase();if(provider==="aws")await requestAwsPort25(options,domain);else if(provider==="hetzner")await requestHetznerPort25(options,domain);else{log.error(`Unknown provider: ${provider}. Use 'aws' or 'hetzner'.`);process.exit(ExitCode.FatalError)}});buddy.command("mail:port25:status","Check outbound port 25 status on the mail server").option("--provider <provider>","Cloud provider: aws or hetzner",{default:"aws"}).action(async(options)=>{loadEnvFiles();const region=process.env.AWS_REGION||"us-east-1",appName=(process.env.APP_NAME||"stacks").toLowerCase().replace(/[^a-z0-9-]/g,"-");if(options.provider==="hetzner"){const hetznerToken=process.env.HETZNER_API_TOKEN;if(!hetznerToken){log.error("HETZNER_API_TOKEN not set");process.exit(ExitCode.FatalError)}try{const ip=(await(await fetch("https://api.hetzner.cloud/v1/servers?label_selector=service=mail",{headers:{Authorization:`Bearer ${hetznerToken}`}})).json()).servers?.[0]?.public_net?.ipv4?.ip;if(!ip){const mailServer=(await(await fetch("https://api.hetzner.cloud/v1/servers",{headers:{Authorization:`Bearer ${hetznerToken}`}})).json()).servers?.find((s)=>s.name?.includes("mail"));if(mailServer){const serverIp=mailServer.public_net?.ipv4?.ip;log.info(`Found Hetzner server: ${mailServer.name} (${serverIp})`);const result=execSync(`ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@${serverIp} "timeout 5 bash -c 'echo QUIT > /dev/tcp/gmail-smtp-in.l.google.com/25' 2>&1 && echo PORT_25_OPEN || echo PORT_25_BLOCKED"`,{encoding:"utf-8"});printPort25Status(result.trim(),"Hetzner",serverIp)}else log.error("No mail server found on Hetzner")}else{const result=execSync(`ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@${ip} "timeout 5 bash -c 'echo QUIT > /dev/tcp/gmail-smtp-in.l.google.com/25' 2>&1 && echo PORT_25_OPEN || echo PORT_25_BLOCKED"`,{encoding:"utf-8"});printPort25Status(result.trim(),"Hetzner",ip)}}catch(error){log.error(`Failed: ${getErrorMessage(error)}`)}process.exit(ExitCode.Success)}try{const{EC2Client,SSMClient}=await import("@stacksjs/ts-cloud"),ec2=new EC2Client(region),ssm=new SSMClient(region),instance=(await ec2.describeInstances({Filters:[{Name:"tag:Name",Values:[`${appName}-mail-server`]},{Name:"instance-state-name",Values:["running"]}]})).Reservations?.[0]?.Instances?.[0],instanceId=instance?.InstanceId;if(!instanceId){log.error("No running mail server found.");process.exit(ExitCode.FatalError)}const publicIp=instance?.PublicIpAddress||"unknown";log.info(`Testing port 25 on ${instanceId} (${publicIp})...`);const result=await ssm.runShellCommand(instanceId,['timeout 5 bash -c "echo QUIT > /dev/tcp/gmail-smtp-in.l.google.com/25" 2>&1 && echo PORT_25_OPEN || echo PORT_25_BLOCKED'],{timeoutSeconds:15,maxWaitMs:20000,pollIntervalMs:2000});printPort25Status(result.output?.trim()||"PORT_25_BLOCKED","AWS",publicIp);process.exit(ExitCode.Success)}catch(error){log.error(`Failed: ${getErrorMessage(error)}`);process.exit(ExitCode.FatalError)}});buddy.command("mail:server","Start SMTP relay server").option("--port <port>","SMTP port",{default:"587"}).action(async(options)=>{loadEnvFiles();const domain=process.env.APP_DOMAIN||process.env.MAIL_DOMAIN||"stacksjs.com",port=Number.parseInt(options.port,10),mailboxes=["chris","blake","glenn"],users={},generated=[];for(const name of mailboxes){const envKey=`MAIL_PASSWORD_${name.toUpperCase()}`;let password=process.env[envKey];if(!password){password=randomBytes(16).toString("base64url");generated.push(`${envKey}=${password}`)}users[name]={password,email:`${name}@${domain}`}}if(generated.length>0){console.log("");log.warn("Generated missing passwords. Add these to .env.production:");for(const line of generated)console.log(` ${line}`)}console.log("");console.log("==========================================================");console.log(" SMTP RELAY SERVER");console.log("==========================================================");console.log("");console.log(` Host: mail.${domain}`);console.log(` Port: ${port}`);console.log(` Domain: ${domain}`);console.log("");console.log(" Mailboxes:");for(const[name,user]of Object.entries(users))console.log(` ${name} (${user.email})`);console.log("");console.log(" Relaying through AWS SES");console.log("==========================================================");console.log("");try{const{SmtpServer}=await import("../../../cloud/src/imap/smtp-server"),server=new SmtpServer({port,host:"0.0.0.0",domain,users,sentBucket:"stacks-production-email",sentPrefix:"sent/"});await server.start();await new Promise((resolve)=>{process.on("SIGINT",async()=>{console.log(`
63
- Shutting down SMTP server...`);await server.stop();resolve()})});process.exit(0)}catch(error){log.error(`Failed to start SMTP server: ${getErrorMessage(error)}`);process.exit(ExitCode.FatalError)}});buddy.command("mail:preview","Open the dev-only Mailable preview UI in your browser").option("--no-open","Print the URL but skip the browser-open step",{default:!1}).option("--port <port>","Override the API port (default: from config.ports.api / PORT_API)").example("buddy mail:preview").example("buddy mail:preview --no-open").example("buddy mail:preview --port 4000").action(async(options)=>{const{config}=await import("@stacksjs/config"),portFromOption=options.port?Number(options.port):void 0,portFromEnv=process.env.PORT_API?Number(process.env.PORT_API):void 0,url=`http://localhost:${portFromOption||portFromEnv||config.ports?.api||3008}/_stacks/mail/preview`;log.info(`Mailable preview: ${url}`);log.info("(Make sure `./buddy dev:api` is running.)");if(options.open!==!1)try{execSync(`open "${url}" 2>/dev/null || xdg-open "${url}" 2>/dev/null || true`,{encoding:"utf-8"})}catch{}process.exit(ExitCode.Success)});buddy.command("mail:dev","Run the local mail catcher: accepts every message, delivers none, shows them in the webmail UI").option("--port <port>",`SMTP port (default: ${LOCAL_MAIL_SMTP_PORT})`).option("--ui-port <port>",`Webmail UI port (default: ${LOCAL_MAIL_UI_PORT})`).option("--no-open","Do not open the webmail UI in a browser",{default:!1}).example("buddy mail:dev").example("buddy mail:dev --port 2525 --ui-port 8080").action(async(options)=>{loadEnvFiles();await runLocalMailCatcher({smtpPort:Number(options.port)||LOCAL_MAIL_SMTP_PORT,uiPort:Number(options.uiPort)||LOCAL_MAIL_UI_PORT,open:options.open!==!1})})}export const LOCAL_MAIL_SMTP_PORT=1025,LOCAL_MAIL_UI_PORT=8025,LOCAL_MAIL_DIR="storage/framework/mail";export function resolveMailBinary(root=process.cwd()){const local=`${root}/pantry/.bin/mail`;if(existsSync(local))return local;try{const found=execSync("command -v mail 2>/dev/null",{encoding:"utf-8"}).trim();if(found&&execSync(`${shellQuote(found)} --help 2>&1 || true`,{encoding:"utf-8"}).includes("Mail server management CLI"))return found}catch{}return}export function buildLocalMailConfig(options){return["# Generated by `buddy mail:dev`. Edits are overwritten on the next run.","","[server]",'host = "127.0.0.1"',`port = ${options.smtpPort}`,'hostname = "localhost"',"","[tls]","enabled = false","","[auth]","enabled = false","","[antispam]","spam_filter = false","dnsbl = false","greylist = false",""].join(`
64
- `)}async function runLocalMailCatcher(options){const binary=resolveMailBinary();if(!binary){log.error("The mail server is not installed.");log.info("Install it with: pantry install github.com/mail-os/mail");log.info("(It is declared in config/deps.ts, so `pantry install` on its own is enough.)");process.exit(ExitCode.FatalError)}const dir=`${process.cwd()}/${LOCAL_MAIL_DIR}`,configPath=`${dir}/catcher.toml`;await runCommand(`mkdir -p ${shellQuote(dir)}`,{silent:!0});await Bun.write(configPath,buildLocalMailConfig({smtpPort:options.smtpPort}));console.log("");console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");console.log(" Local mail catcher");console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");console.log(` SMTP 127.0.0.1:${options.smtpPort}`);console.log(` Webmail http://localhost:${options.uiPort}`);console.log(` Mailbox ${LOCAL_MAIL_DIR}`);console.log("");console.log(" Nothing is delivered onward. Ctrl-C to stop.");console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");console.log("");if(options.open)try{execSync(`open "http://localhost:${options.uiPort}" 2>/dev/null || xdg-open "http://localhost:${options.uiPort}" 2>/dev/null || true`,{encoding:"utf-8"})}catch{}const child=Bun.spawn([binary,"serve","--config",configPath],{cwd:dir,env:{...process.env,SMTP_DB_PATH:`${dir}/catcher.db`,SMTP_ENABLE_WEBMAIL:"true",SMTP_WEBMAIL_PORT:String(options.uiPort),SMTP_WEBMAIL_SECURE_COOKIES:"false"},stdout:"inherit",stderr:"inherit"});process.on("SIGINT",()=>{child.kill();process.exit(ExitCode.Success)});process.exit(await child.exited)}function loadEnvFiles(){const envPath=".env.production";if(existsSync(envPath))try{const{loadEnv}=require("@stacksjs/env");loadEnv({path:envPath,env:"production",keysFile:".env.keys",overload:!1,quiet:!0})}catch{const content=readFileSync(envPath,"utf-8");for(const line of content.split(`
63
+ Shutting down SMTP server...`);await server.stop();resolve()})});process.exit(0)}catch(error){log.error(`Failed to start SMTP server: ${getErrorMessage(error)}`);process.exit(ExitCode.FatalError)}});buddy.command("mail:preview","Open the dev-only Mailable preview UI in your browser").option("--no-open","Print the URL but skip the browser-open step",{default:!1}).option("--port <port>","Override the API port (default: from config.ports.api / PORT_API)").example("buddy mail:preview").example("buddy mail:preview --no-open").example("buddy mail:preview --port 4000").action(async(options)=>{const{config}=await import("@stacksjs/config"),portFromOption=options.port?Number(options.port):void 0,portFromEnv=process.env.PORT_API?Number(process.env.PORT_API):void 0,url=`http://localhost:${portFromOption||portFromEnv||config.ports?.api||3008}/_stacks/mail/preview`;log.info(`Mailable preview: ${url}`);log.info("(Make sure `./buddy dev:api` is running.)");if(options.open!==!1)try{execSync(`open "${url}" 2>/dev/null || xdg-open "${url}" 2>/dev/null || true`,{encoding:"utf-8"})}catch{}process.exit(ExitCode.Success)});buddy.command("mail:dev","Run the local mail catcher: accepts every message, delivers none, shows them in the webmail UI").option("--port <port>",`SMTP port (default: ${LOCAL_MAIL_SMTP_PORT})`).option("--ui-port <port>",`Webmail UI port (default: ${LOCAL_MAIL_UI_PORT})`).option("--no-open","Do not open the webmail UI in a browser",{default:!1}).example("buddy mail:dev").example("buddy mail:dev --port 2525 --ui-port 8080").action(async(options)=>{loadEnvFiles();await runLocalMailCatcher({smtpPort:Number(options.port)||LOCAL_MAIL_SMTP_PORT,uiPort:Number(options.uiPort)||LOCAL_MAIL_UI_PORT,open:options.open!==!1})})}export const LOCAL_MAIL_SMTP_PORT=1025,LOCAL_MAIL_UI_PORT=8025,LOCAL_MAIL_DIR="storage/framework/mail";export function resolveMailBinary(root=process.cwd()){const local=`${root}/pantry/.bin/mail`;if(existsSync(local))return local;try{const found=execSync("command -v mail 2>/dev/null",{encoding:"utf-8"}).trim();if(found&&isMailServer(found))return found}catch{}return}function isMailServer(binary){try{return execSync(`${shellQuote(binary)} </dev/null 2>&1 || true`,{encoding:"utf-8",timeout:5000}).includes("Mail server management CLI")}catch{return!1}}export function buildLocalMailConfig(options){return["# Generated by `buddy mail:dev`. Edits are overwritten on the next run.","","[server]",'host = "127.0.0.1"',`port = ${options.smtpPort}`,'hostname = "localhost"',"catch_all = true","","[tls]","enabled = false","","[auth]","enabled = false","","[antispam]","spam_filter = false","dnsbl = false","greylist = false",""].join(`
64
+ `)}async function runLocalMailCatcher(options){const binary=resolveMailBinary();if(!binary){log.error("The mail server is not installed.");log.info("Install it with: pantry install github.com/mail-os/mail");log.info("(It is declared in config/deps.ts, so `pantry install` on its own is enough.)");process.exit(ExitCode.FatalError)}const dir=`${process.cwd()}/${LOCAL_MAIL_DIR}`,configPath=`${dir}/catcher.toml`;await runCommand(`mkdir -p ${shellQuote(dir)}`,{silent:!0});await Bun.write(configPath,buildLocalMailConfig({smtpPort:options.smtpPort}));console.log("");console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");console.log(" Local mail catcher");console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");console.log(` SMTP 127.0.0.1:${options.smtpPort}`);console.log(` Webmail http://localhost:${options.uiPort} (sign in: dev / dev)`);console.log(` Mailbox ${LOCAL_MAIL_DIR}`);console.log("");console.log(" Every recipient is accepted; nothing is delivered onward.");console.log(" Ctrl-C to stop.");console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");console.log("");if(options.open)try{execSync(`open "http://localhost:${options.uiPort}" 2>/dev/null || xdg-open "http://localhost:${options.uiPort}" 2>/dev/null || true`,{encoding:"utf-8"})}catch{}const child=Bun.spawn([binary,"serve","--config",configPath],{cwd:dir,env:{...process.env,SMTP_DB_PATH:`${dir}/catcher.db`,SMTP_ENABLE_WEBMAIL:"true",SMTP_WEBMAIL_PORT:String(options.uiPort),SMTP_WEBMAIL_SECURE_COOKIES:"false"},stdout:"inherit",stderr:"inherit"});process.on("SIGINT",()=>{child.kill();process.exit(ExitCode.Success)});process.exit(await child.exited)}function loadEnvFiles(){const envPath=".env.production";if(existsSync(envPath))try{const{loadEnv}=require("@stacksjs/env");loadEnv({path:envPath,env:"production",keysFile:".env.keys",overload:!1,quiet:!0})}catch{const content=readFileSync(envPath,"utf-8");for(const line of content.split(`
65
65
  `)){const eq=line.indexOf("=");if(eq>0&&!line.startsWith("#")){const key=line.slice(0,eq).trim(),value=line.slice(eq+1).trim();if(!process.env[key])process.env[key]=value}}}if(!process.env.AWS_ACCESS_KEY_ID||!process.env.AWS_SECRET_ACCESS_KEY){const awsCredsPath=`${process.env.HOME||process.env.USERPROFILE||""}/.aws/credentials`;if(existsSync(awsCredsPath)){const credsContent=readFileSync(awsCredsPath,"utf-8"),profile=process.env.AWS_PROFILE||"default",lines=credsContent.split(`
66
66
  `);let inProfile=!1;for(const line of lines){const trimmed=line.trim();if(trimmed.startsWith("[")){inProfile=trimmed===`[${profile}]`;continue}if(inProfile&&trimmed.includes("=")){const eq=trimmed.indexOf("="),key=trimmed.slice(0,eq).trim(),value=trimmed.slice(eq+1).trim();if(key==="aws_access_key_id")process.env.AWS_ACCESS_KEY_ID=value;if(key==="aws_secret_access_key")process.env.AWS_SECRET_ACCESS_KEY=value}}}}}function printPort25Status(result,provider,ip){console.log("");console.log("==========================================================");console.log(" PORT 25 STATUS");console.log("==========================================================");console.log("");console.log(` Provider: ${provider}`);console.log(` Server IP: ${ip}`);if(result.includes("PORT_25_OPEN")){console.log(" Port 25: \x1B[32mOPEN\x1B[0m");console.log("");console.log(" Direct SMTP delivery is available!");console.log(" Set SMTP_DELIVERY_METHOD=direct in your server env.")}else{console.log(" Port 25: \x1B[31mBLOCKED\x1B[0m");console.log("");console.log(` Run: buddy mail:port25:request --provider ${provider.toLowerCase()}`)}console.log("");console.log("==========================================================");console.log("")}async function requestAwsPort25(options,domain){const region=process.env.AWS_REGION||"us-east-1",appName=(process.env.APP_NAME||"stacks").toLowerCase().replace(/[^a-z0-9-]/g,"-");let{instanceId,elasticIp}=options;if(!instanceId||!elasticIp)try{const{EC2Client}=await import("@stacksjs/ts-cloud"),instance=(await new EC2Client(region).describeInstances({Filters:[{Name:"tag:Name",Values:[`${appName}-mail-server`]},{Name:"instance-state-name",Values:["running"]}]})).Reservations?.[0]?.Instances?.[0];if(instance){instanceId=instanceId||instance.InstanceId;elasticIp=elasticIp||instance.PublicIpAddress}}catch{}const rdns=options.rdns||`mail.${domain}`,useCase=options.useCase||[`We run a self-hosted mail server (mail.${domain}) for our organization.`,"We send transactional emails (account notifications, password resets) and","occasional team communication. Expected volume: <1000 emails/day.","We have SPF, DKIM, and DMARC configured. We need outbound port 25","to deliver email directly to recipient mail servers via MX records."].join(" ");console.log("");console.log("==========================================================");console.log(" AWS PORT 25 UNBLOCK REQUEST");console.log("==========================================================");console.log("");console.log(` Instance: ${instanceId||"(not detected)"}`);console.log(` Elastic IP: ${elasticIp||"(not detected)"}`);console.log(` Reverse DNS: ${rdns}`);console.log(` Region: ${region}`);console.log("");try{const result=execSync(`aws support create-case --subject "Request to remove email sending limitations" --communication-body "${useCase}
67
67
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.41",
5
+ "version": "0.72.42",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -95,57 +95,57 @@
95
95
  "prepublishOnly": "bun run build"
96
96
  },
97
97
  "dependencies": {
98
- "@stacksjs/actions": "^0.72.41",
99
- "@stacksjs/ai": "^0.72.41",
100
- "@stacksjs/alias": "^0.72.41",
101
- "@stacksjs/arrays": "^0.72.41",
102
- "@stacksjs/auth": "^0.72.41",
103
- "@stacksjs/build": "^0.72.41",
104
- "@stacksjs/cache": "^0.72.41",
105
- "@stacksjs/chat": "^0.72.41",
98
+ "@stacksjs/actions": "^0.72.42",
99
+ "@stacksjs/ai": "^0.72.42",
100
+ "@stacksjs/alias": "^0.72.42",
101
+ "@stacksjs/arrays": "^0.72.42",
102
+ "@stacksjs/auth": "^0.72.42",
103
+ "@stacksjs/build": "^0.72.42",
104
+ "@stacksjs/cache": "^0.72.42",
105
+ "@stacksjs/chat": "^0.72.42",
106
106
  "@stacksjs/clapp": "^0.2.12",
107
- "@stacksjs/cli": "^0.72.41",
108
- "@stacksjs/cloud": "^0.72.41",
109
- "@stacksjs/cms": "^0.72.41",
110
- "@stacksjs/collections": "^0.72.41",
111
- "@stacksjs/config": "^0.72.41",
112
- "@stacksjs/database": "^0.72.41",
113
- "@stacksjs/desktop-build": "^0.72.41",
114
- "@stacksjs/dns": "^0.72.41",
107
+ "@stacksjs/cli": "^0.72.42",
108
+ "@stacksjs/cloud": "^0.72.42",
109
+ "@stacksjs/cms": "^0.72.42",
110
+ "@stacksjs/collections": "^0.72.42",
111
+ "@stacksjs/config": "^0.72.42",
112
+ "@stacksjs/database": "^0.72.42",
113
+ "@stacksjs/desktop-build": "^0.72.42",
114
+ "@stacksjs/dns": "^0.72.42",
115
115
  "@stacksjs/dnsx": "^0.2.3",
116
- "@stacksjs/email": "^0.72.41",
117
- "@stacksjs/enums": "^0.72.41",
118
- "@stacksjs/error-handling": "^0.72.41",
119
- "@stacksjs/events": "^0.72.41",
120
- "@stacksjs/git": "^0.72.41",
116
+ "@stacksjs/email": "^0.72.42",
117
+ "@stacksjs/enums": "^0.72.42",
118
+ "@stacksjs/error-handling": "^0.72.42",
119
+ "@stacksjs/events": "^0.72.42",
120
+ "@stacksjs/git": "^0.72.42",
121
121
  "@stacksjs/gitit": "^0.2.5",
122
- "@stacksjs/health": "^0.72.41",
122
+ "@stacksjs/health": "^0.72.42",
123
123
  "@stacksjs/httx": "^0.1.10",
124
- "@stacksjs/image": "^0.72.41",
125
- "@stacksjs/lint": "^0.72.41",
126
- "@stacksjs/logging": "^0.72.41",
127
- "@stacksjs/notifications": "^0.72.41",
128
- "@stacksjs/objects": "^0.72.41",
129
- "@stacksjs/orm": "^0.72.41",
130
- "@stacksjs/path": "^0.72.41",
131
- "@stacksjs/payments": "^0.72.41",
132
- "@stacksjs/realtime": "^0.72.41",
133
- "@stacksjs/router": "^0.72.41",
124
+ "@stacksjs/image": "^0.72.42",
125
+ "@stacksjs/lint": "^0.72.42",
126
+ "@stacksjs/logging": "^0.72.42",
127
+ "@stacksjs/notifications": "^0.72.42",
128
+ "@stacksjs/objects": "^0.72.42",
129
+ "@stacksjs/orm": "^0.72.42",
130
+ "@stacksjs/path": "^0.72.42",
131
+ "@stacksjs/payments": "^0.72.42",
132
+ "@stacksjs/realtime": "^0.72.42",
133
+ "@stacksjs/router": "^0.72.42",
134
134
  "@stacksjs/rpx": "^0.11.42",
135
- "@stacksjs/search-engine": "^0.72.41",
136
- "@stacksjs/security": "^0.72.41",
137
- "@stacksjs/server": "^0.72.41",
138
- "@stacksjs/sites": "^0.72.41",
139
- "@stacksjs/skills": "^0.72.41",
140
- "@stacksjs/storage": "^0.72.41",
141
- "@stacksjs/strings": "^0.72.41",
142
- "@stacksjs/testing": "^0.72.41",
135
+ "@stacksjs/search-engine": "^0.72.42",
136
+ "@stacksjs/security": "^0.72.42",
137
+ "@stacksjs/server": "^0.72.42",
138
+ "@stacksjs/sites": "^0.72.42",
139
+ "@stacksjs/skills": "^0.72.42",
140
+ "@stacksjs/storage": "^0.72.42",
141
+ "@stacksjs/strings": "^0.72.42",
142
+ "@stacksjs/testing": "^0.72.42",
143
143
  "@stacksjs/ts-cloud": "^0.9.4",
144
- "@stacksjs/tunnel": "^0.72.41",
145
- "@stacksjs/types": "^0.72.41",
146
- "@stacksjs/ui": "^0.72.41",
147
- "@stacksjs/utils": "^0.72.41",
148
- "@stacksjs/validation": "^0.72.41",
144
+ "@stacksjs/tunnel": "^0.72.42",
145
+ "@stacksjs/types": "^0.72.42",
146
+ "@stacksjs/ui": "^0.72.42",
147
+ "@stacksjs/utils": "^0.72.42",
148
+ "@stacksjs/validation": "^0.72.42",
149
149
  "ajv": "^8.20.0",
150
150
  "ajv-formats": "^3.0.1",
151
151
  "ts-pantry": "^0.11.35"