send from gmail

This commit is contained in:
Ken Yasue
2025-04-01 16:19:00 +02:00
parent 45164faa9d
commit 597f7ddc55
6 changed files with 129 additions and 10 deletions

4
.gitignore vendored
View File

@ -136,4 +136,6 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
data data
public/uploads

View File

@ -0,0 +1,6 @@
Please do following
- checkout to new brach features/gmail_sender
- create email.ts
- create function to send email usimg gmail smtp
- creadentials are in .env
- the function receives CustomerId and save to the Record

20
package-lock.json generated
View File

@ -18,11 +18,13 @@
"@editorjs/marker": "^1.4.0", "@editorjs/marker": "^1.4.0",
"@editorjs/paragraph": "^2.11.7", "@editorjs/paragraph": "^2.11.7",
"@editorjs/quote": "^2.7.6", "@editorjs/quote": "^2.7.6",
"@types/nodemailer": "^6.4.17",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"csv-parse": "^5.6.0", "csv-parse": "^5.6.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"mysql2": "^3.13.0", "mysql2": "^3.13.0",
"next": "15.2.2", "next": "15.2.2",
"nodemailer": "^6.10.0",
"pg": "^8.14.0", "pg": "^8.14.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
@ -1550,12 +1552,19 @@
"version": "20.17.24", "version": "20.17.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz",
"integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==", "integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.19.2" "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==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.0.10", "version": "19.0.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
@ -5937,6 +5946,14 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0" "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
} }
}, },
"node_modules/nodemailer": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz",
"integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nopt": { "node_modules/nopt": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@ -8133,7 +8150,6 @@
"version": "6.19.8", "version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unique-filename": { "node_modules/unique-filename": {

View File

@ -22,11 +22,13 @@
"@editorjs/marker": "^1.4.0", "@editorjs/marker": "^1.4.0",
"@editorjs/paragraph": "^2.11.7", "@editorjs/paragraph": "^2.11.7",
"@editorjs/quote": "^2.7.6", "@editorjs/quote": "^2.7.6",
"@types/nodemailer": "^6.4.17",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"csv-parse": "^5.6.0", "csv-parse": "^5.6.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"mysql2": "^3.13.0", "mysql2": "^3.13.0",
"next": "15.2.2", "next": "15.2.2",
"nodemailer": "^6.10.0",
"pg": "^8.14.0", "pg": "^8.14.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
@ -48,4 +50,4 @@
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5" "typescript": "^5"
} }
} }

87
src/lib/email.ts Normal file
View File

@ -0,0 +1,87 @@
import nodemailer from 'nodemailer';
import { ContactRecord } from './database/entities/ContactRecord';
import { Customer } from './database/entities/Customer';
import { getDataSource } from './database';
interface SendEmailResult {
success: boolean;
error?: string;
}
export async function sendEmail(
customerId: string,
subject: string,
body: string,
htmlBody?: string
): Promise<SendEmailResult> {
try {
// Get data source
const dataSource = await getDataSource();
// Get customer details
const customer = await dataSource.getRepository(Customer).findOne({
where: { id: customerId }
});
if (!customer) {
throw new Error(`Customer with ID ${customerId} not found`);
}
// Create Gmail transporter
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.GMAIL_USER,
pass: process.env.GMAIL_APP_PASSWORD // Using app-specific password
}
});
// Send email
const info = await transporter.sendMail({
from: process.env.GMAIL_USER,
to: customer.email,
subject: subject,
text: body,
html: htmlBody || body
});
// Create contact record
const contactRecord = new ContactRecord();
contactRecord.customer = customer;
contactRecord.customerId = customer.id;
contactRecord.contactType = 'EMAIL';
contactRecord.notes = `Email sent successfully. Subject: ${subject}. Message ID: ${info.messageId}`;
// Save contact record
await dataSource.getRepository(ContactRecord).save(contactRecord);
return { success: true };
} catch (error) {
// Create contact record for failed attempt
if (error instanceof Error) {
try {
const dataSource = await getDataSource();
const customer = await dataSource.getRepository(Customer).findOne({
where: { id: customerId }
});
if (customer) {
const contactRecord = new ContactRecord();
contactRecord.customer = customer;
contactRecord.customerId = customer.id;
contactRecord.contactType = 'EMAIL';
contactRecord.notes = `Failed to send email: ${error.message}`;
await dataSource.getRepository(ContactRecord).save(contactRecord);
}
} catch (dbError) {
console.error('Failed to save error record:', dbError);
}
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}

View File

@ -8,7 +8,7 @@ import { Customer } from '../lib/database/entities/Customer';
interface CustomerCSVRow { interface CustomerCSVRow {
City: string; City: string;
Name: string; Name: string;
'Website URL': string; URL: string;
Email: string; Email: string;
} }
@ -66,14 +66,20 @@ async function importCustomers(csvFilePath: string): Promise<void> {
// Create new customer // Create new customer
const customer = new Customer(); const customer = new Customer();
customer.name = name; customer.name = name;
customer.url = record['Website URL'] === 'null' ? '' : record['Website URL']; customer.url = record['URL'] === 'null' ? '' : record['URL'];
customer.email = email; customer.email = email;
// Save to database try {
await customerRepository.save(customer);
importedCount++; // Save to database
await customerRepository.save(customer);
importedCount++;
console.log(`Imported customer: ${name}`);
} catch (e) {
console.log(`Skipped: ${name}`);
}
console.log(`Imported customer: ${name}`);
} }
console.log('Import summary:'); console.log('Import summary:');