diff --git a/package-lock.json b/package-lock.json index 44a1abb..e565af8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "genkit": "^1.14.1", "lucide-react": "^0.475.0", "next": "15.3.3", + "nodemailer": "^6.9.14", "patch-package": "^8.0.0", "react": "^18.3.1", "react-day-picker": "^8.10.1", @@ -56,6 +57,7 @@ "devDependencies": { "@types/better-sqlite3": "^7.6.11", "@types/node": "^20", + "@types/nodemailer": "^6.4.15", "@types/react": "^18", "@types/react-dom": "^18", "genkit-cli": "^1.14.1", @@ -4300,6 +4302,16 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -7873,6 +7885,15 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/package.json b/package.json index 317cef9..9cc3384 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "genkit": "^1.14.1", "lucide-react": "^0.475.0", "next": "15.3.3", + "nodemailer": "^6.9.14", "patch-package": "^8.0.0", "react": "^18.3.1", "react-day-picker": "^8.10.1", @@ -61,6 +62,7 @@ "devDependencies": { "@types/better-sqlite3": "^7.6.11", "@types/node": "^20", + "@types/nodemailer": "^6.4.15", "@types/react": "^18", "@types/react-dom": "^18", "genkit-cli": "^1.14.1", diff --git a/src/app/admin/settings/email/page.tsx b/src/app/admin/settings/email/page.tsx index 08548ce..0305987 100644 --- a/src/app/admin/settings/email/page.tsx +++ b/src/app/admin/settings/email/page.tsx @@ -9,12 +9,16 @@ import { Card, CardContent, CardDescription, + CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useToast } from "@/hooks/use-toast"; +import { useState, useTransition } from "react"; +import { sendTestEmail } from "@/lib/actions/email"; +import { Mail } from "lucide-react"; const emailSettingsSchema = z.object({ smtpHost: z.string().min(1, "SMTP Host is required"), @@ -29,9 +33,13 @@ type EmailSettingsFormValues = z.infer; export default function EmailSettingsPage() { const { toast } = useToast(); + const [isSaving, startSavingTransition] = useTransition(); + const [isTesting, setIsTesting] = useState(false); + const { register, handleSubmit, + getValues, formState: { errors }, } = useForm({ resolver: zodResolver(emailSettingsSchema), @@ -46,86 +54,119 @@ export default function EmailSettingsPage() { }); const onSubmit: SubmitHandler = async (data) => { - // Here you would typically send the data to your backend to save it - console.log("Saving email settings:", data); + startSavingTransition(async () => { + // Here you would typically send the data to your backend to save it + console.log("Saving email settings:", data); - // Simulate an API call - await new Promise(resolve => setTimeout(resolve, 1000)); + // Simulate an API call + await new Promise(resolve => setTimeout(resolve, 1000)); - toast({ - title: "Settings Saved", - description: "Your SMTP email settings have been updated successfully.", + toast({ + title: "Settings Saved", + description: "Your SMTP email settings have been updated successfully.", + }); }); }; + const handleTestEmail = async () => { + setIsTesting(true); + try { + const settings = getValues(); + const result = await sendTestEmail(settings); + if (result.success) { + toast({ + title: "Test Email Sent", + description: "The test email was sent successfully.", + }); + } else { + toast({ + title: "Failed to Send Email", + description: result.error || "An unknown error occurred.", + variant: "destructive", + }); + } + } catch (error) { + toast({ + title: "Error", + description: "An unexpected error occurred while sending the test email.", + variant: "destructive", + }); + } finally { + setIsTesting(false); + } + }; + return ( - - Email Settings - - Configure your SMTP settings to send emails from the platform. - - - -
-
-
- - - {errors.smtpHost &&

{errors.smtpHost.message}

} + + + Email Settings + + Configure your SMTP settings to send emails from the platform. + + + +
+
+ + + {errors.smtpHost &&

{errors.smtpHost.message}

} +
+
+ + + {errors.smtpPort &&

{errors.smtpPort.message}

} +
+
+ + + {errors.smtpUsername &&

{errors.smtpUsername.message}

} +
+
+ + + {errors.smtpPassword &&

{errors.smtpPassword.message}

} +
+
+ + + {errors.fromName &&

{errors.fromName.message}

} +
+
+ + + {errors.fromEmail &&

{errors.fromEmail.message}

} +
-
- - - {errors.smtpPort &&

{errors.smtpPort.message}

} -
-
- - - {errors.smtpUsername &&

{errors.smtpUsername.message}

} -
-
- - - {errors.smtpPassword &&

{errors.smtpPassword.message}

} -
-
- - - {errors.fromName &&

{errors.fromName.message}

} -
-
- - - {errors.fromEmail &&

{errors.fromEmail.message}

} -
-
-
- -
- - + + + + + + ); } diff --git a/src/lib/actions/email.ts b/src/lib/actions/email.ts new file mode 100644 index 0000000..1e6e913 --- /dev/null +++ b/src/lib/actions/email.ts @@ -0,0 +1,65 @@ + +'use server'; + +import nodemailer from 'nodemailer'; +import { z } from 'zod'; + +const emailSettingsSchema = z.object({ + smtpHost: z.string(), + smtpPort: z.number(), + smtpUsername: z.string(), + smtpPassword: z.string(), + fromName: z.string(), + fromEmail: z.string().email(), +}); + +type EmailSettings = z.infer; + +export async function sendTestEmail( + settings: EmailSettings +): Promise<{ success: boolean; error?: string }> { + const { smtpHost, smtpPort, smtpUsername, smtpPassword, fromName, fromEmail } = settings; + + try { + const transporter = nodemailer.createTransport({ + host: smtpHost, + port: smtpPort, + secure: smtpPort === 465, // true for 465, false for other ports + auth: { + user: smtpUsername, + pass: smtpPassword, + }, + // In a real app, you might want more robust TLS options + // For example, rejecting unauthorized connections + tls: { + rejectUnauthorized: false + } + }); + + await transporter.verify(); + + const mailOptions = { + from: `"${fromName}" <${fromEmail}>`, + to: fromEmail, // Send the test to the 'from' address + subject: 'SMTP Test Email from EstimateFlow', + text: 'This is a test email to verify your SMTP settings. If you received this, your configuration is correct.', + html: '

This is a test email to verify your SMTP settings. If you received this, your configuration is correct.

', + }; + + await transporter.sendMail(mailOptions); + + return { success: true }; + } catch (error: any) { + console.error('Failed to send test email:', error); + // Provide a more user-friendly error message + let errorMessage = 'An unknown error occurred.'; + if (error.code === 'ECONNREFUSED') { + errorMessage = `Connection refused. Check if the SMTP host (${smtpHost}) and port (${smtpPort}) are correct and accessible.`; + } else if (error.code === 'EAUTH') { + errorMessage = 'Authentication failed. Please check your SMTP username and password.'; + } else if (error.message) { + errorMessage = error.message; + } + return { success: false, error: errorMessage }; + } +}