mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 16:23:23 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d82b3f2276 | ||
| 08f017abed | |||
| af9c5a1e1b | |||
|
|
05f4cd0d7c | ||
| 22190f38fd | |||
| 1ba770942d | |||
| 7f73fa5efc | |||
| 8993d20526 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,4 +4,6 @@ node_modules/
|
|||||||
payload-docs
|
payload-docs
|
||||||
dist/
|
dist/
|
||||||
/dev/payload.db
|
/dev/payload.db
|
||||||
|
/dev/dev.db
|
||||||
|
dev.db
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
|
|||||||
2
dev/.env.local
Normal file
2
dev/.env.local
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
USE_MEMORY_DB=true
|
||||||
|
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||||
310
dev/app/(frontend)/dashboard/page.tsx
Normal file
310
dev/app/(frontend)/dashboard/page.tsx
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
interface EmailStats {
|
||||||
|
total: number
|
||||||
|
sent: number
|
||||||
|
pending: number
|
||||||
|
failed: number
|
||||||
|
processing: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
const [emailStats, setEmailStats] = useState<EmailStats>({
|
||||||
|
total: 0,
|
||||||
|
sent: 0,
|
||||||
|
pending: 0,
|
||||||
|
failed: 0,
|
||||||
|
processing: 0
|
||||||
|
})
|
||||||
|
const [loading, setLoading] = useState<boolean>(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchEmailStats()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchEmailStats = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/test-email')
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.outbox?.emails) {
|
||||||
|
const emails = data.outbox.emails
|
||||||
|
const stats: EmailStats = {
|
||||||
|
total: emails.length,
|
||||||
|
sent: emails.filter((email: any) => email.status === 'sent').length,
|
||||||
|
pending: emails.filter((email: any) => email.status === 'pending').length,
|
||||||
|
failed: emails.filter((email: any) => email.status === 'failed').length,
|
||||||
|
processing: emails.filter((email: any) => email.status === 'processing').length
|
||||||
|
}
|
||||||
|
setEmailStats(stats)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching email statistics:', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatCard = ({ label, value, color, description }: { label: string; value: number; color: string; description: string }) => (
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '24px',
|
||||||
|
textAlign: 'center',
|
||||||
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '3rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: color,
|
||||||
|
marginBottom: '8px'
|
||||||
|
}}>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '1.1rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#374151',
|
||||||
|
marginBottom: '4px'
|
||||||
|
}}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
color: '#6b7280'
|
||||||
|
}}>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: '#f9fafb',
|
||||||
|
padding: '40px 20px',
|
||||||
|
minHeight: 'calc(100vh - 80px)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
maxWidth: '1200px',
|
||||||
|
margin: '0 auto'
|
||||||
|
}}>
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{ textAlign: 'center', marginBottom: '48px' }}>
|
||||||
|
<h1 style={{
|
||||||
|
fontSize: '3rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#1f2937',
|
||||||
|
marginBottom: '16px'
|
||||||
|
}}>
|
||||||
|
📧 PayloadCMS Mailing Plugin
|
||||||
|
</h1>
|
||||||
|
<p style={{
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
color: '#6b7280',
|
||||||
|
marginBottom: '24px'
|
||||||
|
}}>
|
||||||
|
Development Dashboard
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '16px', justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||||
|
<Link
|
||||||
|
href="/admin"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
color: 'white',
|
||||||
|
padding: '12px 24px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontWeight: '500',
|
||||||
|
transition: 'background-color 0.2s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
📊 Admin Panel
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/mailing-test"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#10b981',
|
||||||
|
color: 'white',
|
||||||
|
padding: '12px 24px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontWeight: '500',
|
||||||
|
transition: 'background-color 0.2s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🧪 Test Interface
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email Statistics */}
|
||||||
|
<div style={{ marginBottom: '48px' }}>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '24px'
|
||||||
|
}}>
|
||||||
|
<h2 style={{
|
||||||
|
fontSize: '2rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#1f2937'
|
||||||
|
}}>
|
||||||
|
Email Statistics
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={fetchEmailStats}
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
backgroundColor: loading ? '#9ca3af' : '#6b7280',
|
||||||
|
color: 'white',
|
||||||
|
padding: '8px 16px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: 'none',
|
||||||
|
cursor: loading ? 'not-allowed' : 'pointer',
|
||||||
|
fontWeight: '500'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? 'Loading...' : 'Refresh'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '48px' }}>
|
||||||
|
<div style={{ color: '#6b7280', fontSize: '1.1rem' }}>Loading email statistics...</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
||||||
|
gap: '24px'
|
||||||
|
}}>
|
||||||
|
<StatCard
|
||||||
|
label="Total Emails"
|
||||||
|
value={emailStats.total}
|
||||||
|
color="#1f2937"
|
||||||
|
description="All emails in the system"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Successfully Sent"
|
||||||
|
value={emailStats.sent}
|
||||||
|
color="#10b981"
|
||||||
|
description="Delivered successfully"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Pending"
|
||||||
|
value={emailStats.pending}
|
||||||
|
color="#f59e0b"
|
||||||
|
description="Waiting to be sent"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Failed"
|
||||||
|
value={emailStats.failed}
|
||||||
|
color="#ef4444"
|
||||||
|
description="Failed to send"
|
||||||
|
/>
|
||||||
|
{emailStats.processing > 0 && (
|
||||||
|
<StatCard
|
||||||
|
label="Processing"
|
||||||
|
value={emailStats.processing}
|
||||||
|
color="#3b82f6"
|
||||||
|
description="Currently being sent"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '32px',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
|
||||||
|
}}>
|
||||||
|
<h3 style={{
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#1f2937',
|
||||||
|
marginBottom: '16px'
|
||||||
|
}}>
|
||||||
|
Quick Actions
|
||||||
|
</h3>
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
||||||
|
gap: '16px'
|
||||||
|
}}>
|
||||||
|
<div style={{ padding: '16px', backgroundColor: '#f9fafb', borderRadius: '8px' }}>
|
||||||
|
<h4 style={{ marginBottom: '8px', color: '#1f2937' }}>🎯 Test Email Sending</h4>
|
||||||
|
<p style={{ color: '#6b7280', marginBottom: '12px', fontSize: '0.9rem' }}>
|
||||||
|
Send test emails using templates with the interactive testing interface.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/mailing-test"
|
||||||
|
style={{
|
||||||
|
color: '#3b82f6',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontWeight: '500'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Open Test Interface →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ padding: '16px', backgroundColor: '#f9fafb', borderRadius: '8px' }}>
|
||||||
|
<h4 style={{ marginBottom: '8px', color: '#1f2937' }}>📝 Manage Templates</h4>
|
||||||
|
<p style={{ color: '#6b7280', marginBottom: '12px', fontSize: '0.9rem' }}>
|
||||||
|
Create and edit email templates in the Payload admin interface.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/admin/collections/email-templates"
|
||||||
|
style={{
|
||||||
|
color: '#3b82f6',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontWeight: '500'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Manage Templates →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ padding: '16px', backgroundColor: '#f9fafb', borderRadius: '8px' }}>
|
||||||
|
<h4 style={{ marginBottom: '8px', color: '#1f2937' }}>📬 Email Queue</h4>
|
||||||
|
<p style={{ color: '#6b7280', marginBottom: '12px', fontSize: '0.9rem' }}>
|
||||||
|
View and manage the email outbox and delivery status.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/admin/collections/emails"
|
||||||
|
style={{
|
||||||
|
color: '#3b82f6',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontWeight: '500'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View Email Queue →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: '48px',
|
||||||
|
padding: '24px',
|
||||||
|
color: '#6b7280',
|
||||||
|
fontSize: '0.875rem'
|
||||||
|
}}>
|
||||||
|
PayloadCMS Mailing Plugin Development Environment
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
24
dev/app/(frontend)/layout.tsx
Normal file
24
dev/app/(frontend)/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'PayloadCMS Mailing Plugin - Development',
|
||||||
|
description: 'Development environment for PayloadCMS Mailing Plugin',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FrontendLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body style={{
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -33,6 +33,8 @@ export default function MailingTestPage() {
|
|||||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('')
|
const [selectedTemplate, setSelectedTemplate] = useState<string>('')
|
||||||
const [toEmail, setToEmail] = useState<string>('test@example.com')
|
const [toEmail, setToEmail] = useState<string>('test@example.com')
|
||||||
const [variables, setVariables] = useState<Record<string, any>>({})
|
const [variables, setVariables] = useState<Record<string, any>>({})
|
||||||
|
const [jsonVariables, setJsonVariables] = useState<string>('{}')
|
||||||
|
const [jsonError, setJsonError] = useState<string>('')
|
||||||
const [emailType, setEmailType] = useState<'send' | 'schedule'>('send')
|
const [emailType, setEmailType] = useState<'send' | 'schedule'>('send')
|
||||||
const [scheduleDate, setScheduleDate] = useState<string>('')
|
const [scheduleDate, setScheduleDate] = useState<string>('')
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
@@ -58,6 +60,23 @@ export default function MailingTestPage() {
|
|||||||
const template = templates.find(t => t.slug === templateSlug)
|
const template = templates.find(t => t.slug === templateSlug)
|
||||||
if (template?.previewData) {
|
if (template?.previewData) {
|
||||||
setVariables(template.previewData)
|
setVariables(template.previewData)
|
||||||
|
setJsonVariables(JSON.stringify(template.previewData, null, 2))
|
||||||
|
} else {
|
||||||
|
setVariables({})
|
||||||
|
setJsonVariables('{}')
|
||||||
|
}
|
||||||
|
setJsonError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleJsonVariablesChange = (jsonString: string) => {
|
||||||
|
setJsonVariables(jsonString)
|
||||||
|
setJsonError('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(jsonString)
|
||||||
|
setVariables(parsed)
|
||||||
|
} catch (error) {
|
||||||
|
setJsonError(error instanceof Error ? error.message : 'Invalid JSON')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +86,11 @@ export default function MailingTestPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (jsonError) {
|
||||||
|
setMessage('Please fix the JSON syntax error before sending')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setMessage('')
|
setMessage('')
|
||||||
|
|
||||||
@@ -88,7 +112,8 @@ export default function MailingTestPage() {
|
|||||||
const result = await response.json()
|
const result = await response.json()
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setMessage(`✅ ${result.message} (ID: ${result.emailId})`)
|
const statusIcon = result.status === 'sent' ? '📧' : '📫'
|
||||||
|
setMessage(`✅ ${statusIcon} ${result.message} (ID: ${result.emailId})`)
|
||||||
fetchData() // Refresh email queue
|
fetchData() // Refresh email queue
|
||||||
} else {
|
} else {
|
||||||
setMessage(`❌ Error: ${result.error}`)
|
setMessage(`❌ Error: ${result.error}`)
|
||||||
@@ -204,28 +229,43 @@ export default function MailingTestPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedTemplateData?.variables && (
|
{selectedTemplate && (
|
||||||
<div style={{ marginBottom: '15px' }}>
|
<div style={{ marginBottom: '15px' }}>
|
||||||
<h3>Template Variables:</h3>
|
<label style={{ display: 'block', marginBottom: '5px' }}>
|
||||||
{selectedTemplateData.variables.map(variable => (
|
<strong>Template Variables (JSON):</strong>
|
||||||
<div key={variable.name} style={{ marginBottom: '10px' }}>
|
{selectedTemplateData?.variables && (
|
||||||
<label style={{ display: 'block', marginBottom: '5px' }}>
|
<small style={{ color: '#666', marginLeft: '8px' }}>
|
||||||
{variable.name} {variable.required && <span style={{ color: 'red' }}>*</span>}
|
Available variables: {selectedTemplateData.variables.map(v => v.name).join(', ')}
|
||||||
{variable.description && <small style={{ color: '#666' }}> - {variable.description}</small>}
|
</small>
|
||||||
</label>
|
)}
|
||||||
<input
|
</label>
|
||||||
type={variable.type === 'number' ? 'number' : variable.type === 'date' ? 'datetime-local' : 'text'}
|
<textarea
|
||||||
value={variables[variable.name] || ''}
|
value={jsonVariables}
|
||||||
onChange={(e) => setVariables({
|
onChange={(e) => handleJsonVariablesChange(e.target.value)}
|
||||||
...variables,
|
placeholder='{\n "firstName": "John",\n "siteName": "MyApp",\n "createdAt": "2023-01-01T00:00:00Z"\n}'
|
||||||
[variable.name]: variable.type === 'number' ? Number(e.target.value) :
|
style={{
|
||||||
variable.type === 'boolean' ? e.target.checked :
|
width: '100%',
|
||||||
e.target.value
|
height: '150px',
|
||||||
})}
|
padding: '8px',
|
||||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
borderRadius: '4px',
|
||||||
/>
|
border: jsonError ? '2px solid #dc3545' : '1px solid #ddd',
|
||||||
|
fontFamily: 'monaco, "Courier New", monospace',
|
||||||
|
fontSize: '13px',
|
||||||
|
resize: 'vertical'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{jsonError && (
|
||||||
|
<div style={{
|
||||||
|
color: '#dc3545',
|
||||||
|
fontSize: '12px',
|
||||||
|
marginTop: '5px',
|
||||||
|
padding: '5px',
|
||||||
|
backgroundColor: '#f8d7da',
|
||||||
|
borderRadius: '4px'
|
||||||
|
}}>
|
||||||
|
Invalid JSON: {jsonError}
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -4,24 +4,25 @@ import config from '@payload-config'
|
|||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config })
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
// Queue the combined email queue processing job
|
// Run jobs in the default queue (the plugin already schedules email processing on init)
|
||||||
const job = await payload.jobs.queue({
|
const results = await payload.jobs.run({
|
||||||
task: 'process-email-queue',
|
queue: 'default',
|
||||||
input: {},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const processedCount = Array.isArray(results) ? results.length : (results ? 1 : 0)
|
||||||
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Email queue processing job queued successfully (will process both pending and failed emails)',
|
message: `Email queue processing completed. Processed ${processedCount} jobs.`,
|
||||||
jobId: job.id,
|
processedJobs: processedCount,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Process emails error:', error)
|
console.error('Process emails error:', error)
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
{
|
||||||
error: 'Failed to process emails',
|
error: 'Failed to process emails',
|
||||||
details: error instanceof Error ? error.message : 'Unknown error'
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
},
|
},
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getPayload } from 'payload'
|
import { getPayload } from 'payload'
|
||||||
import config from '@payload-config'
|
import config from '@payload-config'
|
||||||
import { sendEmail } from '@xtr-dev/payload-mailing'
|
import { sendEmail, processEmailById } from '@xtr-dev/payload-mailing'
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -8,6 +8,22 @@ export async function POST(request: Request) {
|
|||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { type = 'send', templateSlug, to, variables, scheduledAt, subject, html, text } = body
|
const { type = 'send', templateSlug, to, variables, scheduledAt, subject, html, text } = body
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!to) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: 'Recipient email address (to) is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate email has either template or direct content
|
||||||
|
if (!templateSlug && (!subject || !html)) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: 'Either templateSlug or both subject and html must be provided' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Use the new sendEmail API
|
// Use the new sendEmail API
|
||||||
const emailOptions: any = {
|
const emailOptions: any = {
|
||||||
data: {
|
data: {
|
||||||
@@ -41,10 +57,33 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
const result = await sendEmail(payload, emailOptions)
|
const result = await sendEmail(payload, emailOptions)
|
||||||
|
|
||||||
|
// If it's "send now" (not scheduled), process the email immediately
|
||||||
|
if (type === 'send' && !scheduledAt) {
|
||||||
|
try {
|
||||||
|
await processEmailById(payload, String(result.id))
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
emailId: result.id,
|
||||||
|
message: 'Email sent successfully',
|
||||||
|
status: 'sent'
|
||||||
|
})
|
||||||
|
} catch (processError) {
|
||||||
|
// If immediate processing fails, return that it's queued
|
||||||
|
console.warn('Failed to process email immediately, left in queue:', processError)
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
emailId: result.id,
|
||||||
|
message: 'Email queued successfully (immediate processing failed)',
|
||||||
|
status: 'queued'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
emailId: result.id,
|
emailId: result.id,
|
||||||
message: scheduledAt ? 'Email scheduled successfully' : 'Email queued successfully',
|
message: scheduledAt ? 'Email scheduled successfully' : 'Email queued successfully',
|
||||||
|
status: scheduledAt ? 'scheduled' : 'queued'
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Test email error:', error)
|
console.error('Test email error:', error)
|
||||||
@@ -82,8 +121,8 @@ export async function GET() {
|
|||||||
total: totalDocs,
|
total: totalDocs,
|
||||||
},
|
},
|
||||||
mailing: {
|
mailing: {
|
||||||
pluginActive: !!(payload as any).mailing,
|
pluginActive: 'mailing' in payload && !!payload.mailing,
|
||||||
service: !!(payload as any).mailing?.service,
|
service: 'mailing' in payload && payload.mailing && 'service' in payload.mailing && !!payload.mailing.service,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
11
dev/app/page.tsx
Normal file
11
dev/app/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
import {redirect} from "next/navigation.js"
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'PayloadCMS Mailing Plugin - Development',
|
||||||
|
description: 'Development environment for PayloadCMS Mailing Plugin',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
redirect('/dashboard')
|
||||||
|
}
|
||||||
@@ -90,7 +90,7 @@ export interface Config {
|
|||||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
};
|
};
|
||||||
db: {
|
db: {
|
||||||
defaultIDType: string;
|
defaultIDType: number;
|
||||||
};
|
};
|
||||||
globals: {};
|
globals: {};
|
||||||
globalsSelect: {};
|
globalsSelect: {};
|
||||||
@@ -100,7 +100,7 @@ export interface Config {
|
|||||||
};
|
};
|
||||||
jobs: {
|
jobs: {
|
||||||
tasks: {
|
tasks: {
|
||||||
processEmails: ProcessEmailsJob;
|
'process-emails': ProcessEmailsTask;
|
||||||
'send-email': TaskSendEmail;
|
'send-email': TaskSendEmail;
|
||||||
inline: {
|
inline: {
|
||||||
input: unknown;
|
input: unknown;
|
||||||
@@ -133,7 +133,7 @@ export interface UserAuthOperations {
|
|||||||
* via the `definition` "users".
|
* via the `definition` "users".
|
||||||
*/
|
*/
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: number;
|
||||||
firstName?: string | null;
|
firstName?: string | null;
|
||||||
lastName?: string | null;
|
lastName?: string | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -159,7 +159,7 @@ export interface User {
|
|||||||
* via the `definition` "posts".
|
* via the `definition` "posts".
|
||||||
*/
|
*/
|
||||||
export interface Post {
|
export interface Post {
|
||||||
id: string;
|
id: number;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -168,7 +168,7 @@ export interface Post {
|
|||||||
* via the `definition` "media".
|
* via the `definition` "media".
|
||||||
*/
|
*/
|
||||||
export interface Media {
|
export interface Media {
|
||||||
id: string;
|
id: number;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
@@ -186,7 +186,7 @@ export interface Media {
|
|||||||
* via the `definition` "email-templates".
|
* via the `definition` "email-templates".
|
||||||
*/
|
*/
|
||||||
export interface EmailTemplate {
|
export interface EmailTemplate {
|
||||||
id: string;
|
id: number;
|
||||||
/**
|
/**
|
||||||
* A descriptive name for this email template
|
* A descriptive name for this email template
|
||||||
*/
|
*/
|
||||||
@@ -227,11 +227,11 @@ export interface EmailTemplate {
|
|||||||
* via the `definition` "emails".
|
* via the `definition` "emails".
|
||||||
*/
|
*/
|
||||||
export interface Email {
|
export interface Email {
|
||||||
id: string;
|
id: number;
|
||||||
/**
|
/**
|
||||||
* Email template used (optional if custom content provided)
|
* Email template used (optional if custom content provided)
|
||||||
*/
|
*/
|
||||||
template?: (string | null) | EmailTemplate;
|
template?: (number | null) | EmailTemplate;
|
||||||
/**
|
/**
|
||||||
* Recipient email addresses
|
* Recipient email addresses
|
||||||
*/
|
*/
|
||||||
@@ -316,7 +316,7 @@ export interface Email {
|
|||||||
* via the `definition` "payload-jobs".
|
* via the `definition` "payload-jobs".
|
||||||
*/
|
*/
|
||||||
export interface PayloadJob {
|
export interface PayloadJob {
|
||||||
id: string;
|
id: number;
|
||||||
/**
|
/**
|
||||||
* Input data provided to the job
|
* Input data provided to the job
|
||||||
*/
|
*/
|
||||||
@@ -363,7 +363,7 @@ export interface PayloadJob {
|
|||||||
| {
|
| {
|
||||||
executedAt: string;
|
executedAt: string;
|
||||||
completedAt: string;
|
completedAt: string;
|
||||||
taskSlug: 'inline' | 'processEmails' | 'send-email';
|
taskSlug: 'inline' | 'process-emails' | 'send-email';
|
||||||
taskID: string;
|
taskID: string;
|
||||||
input?:
|
input?:
|
||||||
| {
|
| {
|
||||||
@@ -396,7 +396,7 @@ export interface PayloadJob {
|
|||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
taskSlug?: ('inline' | 'processEmails' | 'send-email') | null;
|
taskSlug?: ('inline' | 'process-emails' | 'send-email') | null;
|
||||||
queue?: string | null;
|
queue?: string | null;
|
||||||
waitUntil?: string | null;
|
waitUntil?: string | null;
|
||||||
processing?: boolean | null;
|
processing?: boolean | null;
|
||||||
@@ -408,36 +408,36 @@ export interface PayloadJob {
|
|||||||
* via the `definition` "payload-locked-documents".
|
* via the `definition` "payload-locked-documents".
|
||||||
*/
|
*/
|
||||||
export interface PayloadLockedDocument {
|
export interface PayloadLockedDocument {
|
||||||
id: string;
|
id: number;
|
||||||
document?:
|
document?:
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
value: string | User;
|
value: number | User;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'posts';
|
relationTo: 'posts';
|
||||||
value: string | Post;
|
value: number | Post;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'media';
|
relationTo: 'media';
|
||||||
value: string | Media;
|
value: number | Media;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'email-templates';
|
relationTo: 'email-templates';
|
||||||
value: string | EmailTemplate;
|
value: number | EmailTemplate;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'emails';
|
relationTo: 'emails';
|
||||||
value: string | Email;
|
value: number | Email;
|
||||||
} | null)
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'payload-jobs';
|
relationTo: 'payload-jobs';
|
||||||
value: string | PayloadJob;
|
value: number | PayloadJob;
|
||||||
} | null);
|
} | null);
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
value: string | User;
|
value: number | User;
|
||||||
};
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -447,10 +447,10 @@ export interface PayloadLockedDocument {
|
|||||||
* via the `definition` "payload-preferences".
|
* via the `definition` "payload-preferences".
|
||||||
*/
|
*/
|
||||||
export interface PayloadPreference {
|
export interface PayloadPreference {
|
||||||
id: string;
|
id: number;
|
||||||
user: {
|
user: {
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
value: string | User;
|
value: number | User;
|
||||||
};
|
};
|
||||||
key?: string | null;
|
key?: string | null;
|
||||||
value?:
|
value?:
|
||||||
@@ -470,7 +470,7 @@ export interface PayloadPreference {
|
|||||||
* via the `definition` "payload-migrations".
|
* via the `definition` "payload-migrations".
|
||||||
*/
|
*/
|
||||||
export interface PayloadMigration {
|
export interface PayloadMigration {
|
||||||
id: string;
|
id: number;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
batch?: number | null;
|
batch?: number | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -628,9 +628,9 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "ProcessEmailsJob".
|
* via the `definition` "ProcessEmailsTask".
|
||||||
*/
|
*/
|
||||||
export interface ProcessEmailsJob {
|
export interface ProcessEmailsTask {
|
||||||
input?: unknown;
|
input?: unknown;
|
||||||
output?: unknown;
|
output?: unknown;
|
||||||
}
|
}
|
||||||
@@ -640,6 +640,10 @@ export interface ProcessEmailsJob {
|
|||||||
*/
|
*/
|
||||||
export interface TaskSendEmail {
|
export interface TaskSendEmail {
|
||||||
input: {
|
input: {
|
||||||
|
/**
|
||||||
|
* Process and send the email immediately instead of waiting for the queue processor
|
||||||
|
*/
|
||||||
|
processImmediately?: boolean | null;
|
||||||
/**
|
/**
|
||||||
* Use a template (leave empty for direct email)
|
* Use a template (leave empty for direct email)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||||
import {
|
import {
|
||||||
FixedToolbarFeature,
|
FixedToolbarFeature,
|
||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
HorizontalRuleFeature,
|
HorizontalRuleFeature,
|
||||||
InlineToolbarFeature,
|
InlineToolbarFeature,
|
||||||
} from '@payloadcms/richtext-lexical'
|
} from '@payloadcms/richtext-lexical'
|
||||||
import { MongoMemoryReplSet } from 'mongodb-memory-server'
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { buildConfig } from 'payload'
|
import { buildConfig } from 'payload'
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
@@ -24,36 +23,7 @@ if (!process.env.ROOT_DIR) {
|
|||||||
process.env.ROOT_DIR = dirname
|
process.env.ROOT_DIR = dirname
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildConfigWithMemoryDB = async () => {
|
export default buildConfig({
|
||||||
// Use in-memory MongoDB for development and testing
|
|
||||||
if (process.env.NODE_ENV === 'test' || process.env.USE_MEMORY_DB === 'true' || !process.env.DATABASE_URI) {
|
|
||||||
console.log('🚀 Starting MongoDB in-memory database...')
|
|
||||||
|
|
||||||
const memoryDB = await MongoMemoryReplSet.create({
|
|
||||||
replSet: {
|
|
||||||
count: 1, // Single instance for dev (faster startup)
|
|
||||||
dbName: process.env.NODE_ENV === 'test' ? 'payloadmemory' : 'payload-mailing-dev',
|
|
||||||
storageEngine: 'wiredTiger',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const uri = `${memoryDB.getUri()}&retryWrites=true`
|
|
||||||
process.env.DATABASE_URI = uri
|
|
||||||
|
|
||||||
console.log('✅ MongoDB in-memory database started')
|
|
||||||
console.log(`📊 Database URI: ${uri.replace(/mongodb:\/\/[^@]*@/, 'mongodb://***@')}`)
|
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
process.on('SIGINT', async () => {
|
|
||||||
console.log('🛑 Stopping MongoDB in-memory database...')
|
|
||||||
await memoryDB.stop()
|
|
||||||
process.exit(0)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.log(`🔗 Using external MongoDB: ${process.env.DATABASE_URI?.replace(/mongodb:\/\/[^@]*@/, 'mongodb://***@')}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildConfig({
|
|
||||||
admin: {
|
admin: {
|
||||||
importMap: {
|
importMap: {
|
||||||
baseDir: path.resolve(dirname),
|
baseDir: path.resolve(dirname),
|
||||||
@@ -122,22 +92,36 @@ const buildConfigWithMemoryDB = async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
db: mongooseAdapter({
|
db: sqliteAdapter({
|
||||||
ensureIndexes: true,
|
client: {
|
||||||
url: process.env.DATABASE_URI || '',
|
url: process.env.DATABASE_URI || 'file:./dev.db',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
editor: lexicalEditor(),
|
editor: lexicalEditor(),
|
||||||
email: testEmailAdapter,
|
email: testEmailAdapter,
|
||||||
onInit: async (payload) => {
|
onInit: async (payload) => {
|
||||||
await seed(payload)
|
await seed(payload)
|
||||||
},
|
},
|
||||||
|
jobs: {
|
||||||
|
jobsCollectionOverrides: c => {
|
||||||
|
if (c.defaultJobsCollection.admin) c.defaultJobsCollection.admin.hidden = false
|
||||||
|
return c.defaultJobsCollection
|
||||||
|
},
|
||||||
|
autoRun: [
|
||||||
|
{
|
||||||
|
cron: '*/1 * * * *', // every minute
|
||||||
|
limit: 10, // limit jobs to process each run
|
||||||
|
queue: 'default', // name of the queue
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
mailingPlugin({
|
mailingPlugin({
|
||||||
defaultFrom: 'noreply@test.com',
|
defaultFrom: 'noreply@test.com',
|
||||||
initOrder: 'after',
|
initOrder: 'after',
|
||||||
retryAttempts: 3,
|
retryAttempts: 3,
|
||||||
retryDelay: 60000, // 1 minute for dev
|
retryDelay: 60000, // 1 minute for dev
|
||||||
queue: 'email-queue',
|
queue: 'default',
|
||||||
|
|
||||||
// Example: Collection overrides for customization
|
// Example: Collection overrides for customization
|
||||||
// Uncomment and modify as needed for your use case
|
// Uncomment and modify as needed for your use case
|
||||||
@@ -286,6 +270,3 @@ const buildConfigWithMemoryDB = async () => {
|
|||||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
export default buildConfigWithMemoryDB()
|
|
||||||
|
|||||||
@@ -3,26 +3,14 @@
|
|||||||
// Development startup script for PayloadCMS Mailing Plugin
|
// Development startup script for PayloadCMS Mailing Plugin
|
||||||
// This ensures proper environment setup and provides helpful information
|
// This ensures proper environment setup and provides helpful information
|
||||||
|
|
||||||
console.log('🚀 PayloadCMS Mailing Plugin - Development Mode')
|
|
||||||
console.log('=' .repeat(50))
|
|
||||||
|
|
||||||
// Set development environment
|
// Set development environment
|
||||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development'
|
process.env.NODE_ENV = process.env.NODE_ENV || 'development'
|
||||||
|
|
||||||
// Enable in-memory MongoDB by default for development
|
// Set default SQLite database for development
|
||||||
if (!process.env.DATABASE_URI) {
|
if (!process.env.DATABASE_URI) {
|
||||||
process.env.USE_MEMORY_DB = 'true'
|
process.env.DATABASE_URI = 'file:./dev.db'
|
||||||
console.log('📦 Using in-memory MongoDB (no installation required)')
|
|
||||||
} else {
|
|
||||||
console.log(`🔗 Using external MongoDB: ${process.env.DATABASE_URI}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('')
|
|
||||||
console.log('🔧 Starting development server...')
|
|
||||||
console.log('📧 Mailing plugin configured with test transport')
|
|
||||||
console.log('🎯 Test interface will be available at: /mailing-test')
|
|
||||||
console.log('')
|
|
||||||
|
|
||||||
// Import and start Next.js
|
// Import and start Next.js
|
||||||
import('next/dist/cli/next-dev.js')
|
import('next/dist/cli/next-dev.js')
|
||||||
.then(({ nextDev }) => {
|
.then(({ nextDev }) => {
|
||||||
@@ -35,11 +23,9 @@ import('next/dist/cli/next-dev.js')
|
|||||||
|
|
||||||
// Handle graceful shutdown
|
// Handle graceful shutdown
|
||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
console.log('\n🛑 Shutting down development server...')
|
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
console.log('\n🛑 Shutting down development server...')
|
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xtr-dev/payload-mailing",
|
"name": "@xtr-dev/payload-mailing",
|
||||||
"version": "0.4.3",
|
"version": "0.4.5",
|
||||||
"description": "Template-based email system with scheduling and job processing for PayloadCMS",
|
"description": "Template-based email system with scheduling and job processing for PayloadCMS",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
@@ -23,9 +23,6 @@
|
|||||||
"dev:generate-importmap": "npm run dev:payload generate:importmap",
|
"dev:generate-importmap": "npm run dev:payload generate:importmap",
|
||||||
"dev:generate-types": "npm run dev:payload generate:types",
|
"dev:generate-types": "npm run dev:payload generate:types",
|
||||||
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
||||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
|
||||||
"generate:importmap": "npm run payload generate:importmap",
|
|
||||||
"generate:types": "npm run payload generate:types",
|
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"lint:fix": "eslint ./src --fix",
|
"lint:fix": "eslint ./src --fix",
|
||||||
"prepublishOnly": "npm run clean && npm run build",
|
"prepublishOnly": "npm run clean && npm run build",
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* This config is only used to generate types.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BaseDatabaseAdapter, buildConfig, Payload} from 'payload'
|
|
||||||
import Emails from "./src/collections/Emails.js"
|
|
||||||
import {createEmailTemplatesCollection} from "./src/collections/EmailTemplates.js"
|
|
||||||
import path from "path"
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
|
||||||
const __dirname = path.dirname(__filename)
|
|
||||||
|
|
||||||
export default buildConfig({
|
|
||||||
collections: [
|
|
||||||
Emails,
|
|
||||||
createEmailTemplatesCollection()
|
|
||||||
],
|
|
||||||
db: {
|
|
||||||
allowIDOnCreate: undefined,
|
|
||||||
defaultIDType: 'number',
|
|
||||||
init: function (args: { payload: Payload; }): BaseDatabaseAdapter {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
},
|
|
||||||
name: undefined
|
|
||||||
},
|
|
||||||
secret: '',
|
|
||||||
typescript: {
|
|
||||||
outputFile: path.resolve(__dirname, 'src/payload-types.ts'),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -19,17 +19,8 @@ export const processEmailsTaskHandler = async (
|
|||||||
const { req } = context
|
const { req } = context
|
||||||
const payload = (req as any).payload
|
const payload = (req as any).payload
|
||||||
|
|
||||||
try {
|
// Use the shared email processing logic
|
||||||
console.log('🔄 Processing email queue (pending + failed emails)...')
|
await processAllEmails(payload)
|
||||||
|
|
||||||
// Use the shared email processing logic
|
|
||||||
await processAllEmails(payload)
|
|
||||||
|
|
||||||
console.log('✅ Email queue processing completed successfully')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Email queue processing failed:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,4 +81,4 @@ export const scheduleEmailsJob = async (
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to schedule email processing job:', error)
|
console.error('Failed to schedule email processing job:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,436 +0,0 @@
|
|||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* This file was automatically generated by Payload.
|
|
||||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
|
||||||
* and re-run `payload generate:types` to regenerate this file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Supported timezones in IANA format.
|
|
||||||
*
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "supportedTimezones".
|
|
||||||
*/
|
|
||||||
export type SupportedTimezones =
|
|
||||||
| 'Pacific/Midway'
|
|
||||||
| 'Pacific/Niue'
|
|
||||||
| 'Pacific/Honolulu'
|
|
||||||
| 'Pacific/Rarotonga'
|
|
||||||
| 'America/Anchorage'
|
|
||||||
| 'Pacific/Gambier'
|
|
||||||
| 'America/Los_Angeles'
|
|
||||||
| 'America/Tijuana'
|
|
||||||
| 'America/Denver'
|
|
||||||
| 'America/Phoenix'
|
|
||||||
| 'America/Chicago'
|
|
||||||
| 'America/Guatemala'
|
|
||||||
| 'America/New_York'
|
|
||||||
| 'America/Bogota'
|
|
||||||
| 'America/Caracas'
|
|
||||||
| 'America/Santiago'
|
|
||||||
| 'America/Buenos_Aires'
|
|
||||||
| 'America/Sao_Paulo'
|
|
||||||
| 'Atlantic/South_Georgia'
|
|
||||||
| 'Atlantic/Azores'
|
|
||||||
| 'Atlantic/Cape_Verde'
|
|
||||||
| 'Europe/London'
|
|
||||||
| 'Europe/Berlin'
|
|
||||||
| 'Africa/Lagos'
|
|
||||||
| 'Europe/Athens'
|
|
||||||
| 'Africa/Cairo'
|
|
||||||
| 'Europe/Moscow'
|
|
||||||
| 'Asia/Riyadh'
|
|
||||||
| 'Asia/Dubai'
|
|
||||||
| 'Asia/Baku'
|
|
||||||
| 'Asia/Karachi'
|
|
||||||
| 'Asia/Tashkent'
|
|
||||||
| 'Asia/Calcutta'
|
|
||||||
| 'Asia/Dhaka'
|
|
||||||
| 'Asia/Almaty'
|
|
||||||
| 'Asia/Jakarta'
|
|
||||||
| 'Asia/Bangkok'
|
|
||||||
| 'Asia/Shanghai'
|
|
||||||
| 'Asia/Singapore'
|
|
||||||
| 'Asia/Tokyo'
|
|
||||||
| 'Asia/Seoul'
|
|
||||||
| 'Australia/Brisbane'
|
|
||||||
| 'Australia/Sydney'
|
|
||||||
| 'Pacific/Guam'
|
|
||||||
| 'Pacific/Noumea'
|
|
||||||
| 'Pacific/Auckland'
|
|
||||||
| 'Pacific/Fiji';
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
auth: {
|
|
||||||
users: UserAuthOperations;
|
|
||||||
};
|
|
||||||
blocks: {};
|
|
||||||
collections: {
|
|
||||||
emails: Email;
|
|
||||||
'email-templates': EmailTemplate;
|
|
||||||
users: User;
|
|
||||||
'payload-locked-documents': PayloadLockedDocument;
|
|
||||||
'payload-preferences': PayloadPreference;
|
|
||||||
'payload-migrations': PayloadMigration;
|
|
||||||
};
|
|
||||||
collectionsJoins: {};
|
|
||||||
collectionsSelect: {
|
|
||||||
emails: EmailsSelect<false> | EmailsSelect<true>;
|
|
||||||
'email-templates': EmailTemplatesSelect<false> | EmailTemplatesSelect<true>;
|
|
||||||
users: UsersSelect<false> | UsersSelect<true>;
|
|
||||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
|
||||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
|
||||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
|
||||||
};
|
|
||||||
db: {
|
|
||||||
defaultIDType: number;
|
|
||||||
};
|
|
||||||
globals: {};
|
|
||||||
globalsSelect: {};
|
|
||||||
locale: null;
|
|
||||||
user: User & {
|
|
||||||
collection: 'users';
|
|
||||||
};
|
|
||||||
jobs: {
|
|
||||||
tasks: unknown;
|
|
||||||
workflows: unknown;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export interface UserAuthOperations {
|
|
||||||
forgotPassword: {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
login: {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
registerFirstUser: {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
unlock: {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Email delivery and status tracking
|
|
||||||
*
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "emails".
|
|
||||||
*/
|
|
||||||
export interface Email {
|
|
||||||
id: number;
|
|
||||||
/**
|
|
||||||
* Email template used (optional if custom content provided)
|
|
||||||
*/
|
|
||||||
template?: (number | null) | EmailTemplate;
|
|
||||||
/**
|
|
||||||
* Recipient email addresses
|
|
||||||
*/
|
|
||||||
to: string[];
|
|
||||||
/**
|
|
||||||
* CC email addresses
|
|
||||||
*/
|
|
||||||
cc?: string[] | null;
|
|
||||||
/**
|
|
||||||
* BCC email addresses
|
|
||||||
*/
|
|
||||||
bcc?: string[] | null;
|
|
||||||
/**
|
|
||||||
* Sender email address (optional, uses default if not provided)
|
|
||||||
*/
|
|
||||||
from?: string | null;
|
|
||||||
/**
|
|
||||||
* Sender display name (optional, e.g., "John Doe" for "John Doe <john@example.com>")
|
|
||||||
*/
|
|
||||||
fromName?: string | null;
|
|
||||||
/**
|
|
||||||
* Reply-to email address
|
|
||||||
*/
|
|
||||||
replyTo?: string | null;
|
|
||||||
/**
|
|
||||||
* Email subject line
|
|
||||||
*/
|
|
||||||
subject: string;
|
|
||||||
/**
|
|
||||||
* Rendered HTML content of the email
|
|
||||||
*/
|
|
||||||
html: string;
|
|
||||||
/**
|
|
||||||
* Plain text version of the email
|
|
||||||
*/
|
|
||||||
text?: string | null;
|
|
||||||
/**
|
|
||||||
* Template variables used to render this email
|
|
||||||
*/
|
|
||||||
variables?:
|
|
||||||
| {
|
|
||||||
[k: string]: unknown;
|
|
||||||
}
|
|
||||||
| unknown[]
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| null;
|
|
||||||
/**
|
|
||||||
* When this email should be sent (leave empty for immediate)
|
|
||||||
*/
|
|
||||||
scheduledAt?: string | null;
|
|
||||||
/**
|
|
||||||
* When this email was actually sent
|
|
||||||
*/
|
|
||||||
sentAt?: string | null;
|
|
||||||
/**
|
|
||||||
* Current status of this email
|
|
||||||
*/
|
|
||||||
status: 'pending' | 'processing' | 'sent' | 'failed';
|
|
||||||
/**
|
|
||||||
* Number of send attempts made
|
|
||||||
*/
|
|
||||||
attempts?: number | null;
|
|
||||||
/**
|
|
||||||
* When the last send attempt was made
|
|
||||||
*/
|
|
||||||
lastAttemptAt?: string | null;
|
|
||||||
/**
|
|
||||||
* Last error message if send failed
|
|
||||||
*/
|
|
||||||
error?: string | null;
|
|
||||||
/**
|
|
||||||
* Email priority (1=highest, 10=lowest)
|
|
||||||
*/
|
|
||||||
priority?: number | null;
|
|
||||||
updatedAt: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "email-templates".
|
|
||||||
*/
|
|
||||||
export interface EmailTemplate {
|
|
||||||
id: number;
|
|
||||||
/**
|
|
||||||
* A descriptive name for this email template
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
/**
|
|
||||||
* Unique identifier for this template (e.g., "welcome-email", "password-reset")
|
|
||||||
*/
|
|
||||||
slug: string;
|
|
||||||
/**
|
|
||||||
* Email subject line. You can use Handlebars variables like {{firstName}} or {{siteName}}.
|
|
||||||
*/
|
|
||||||
subject: string;
|
|
||||||
/**
|
|
||||||
* Email content with rich text formatting. Supports Handlebars variables like {{firstName}} and helpers like {{formatDate createdAt "long"}}. Content is converted to HTML and plain text automatically.
|
|
||||||
*/
|
|
||||||
content: {
|
|
||||||
root: {
|
|
||||||
type: string;
|
|
||||||
children: {
|
|
||||||
type: string;
|
|
||||||
version: number;
|
|
||||||
[k: string]: unknown;
|
|
||||||
}[];
|
|
||||||
direction: ('ltr' | 'rtl') | null;
|
|
||||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
|
||||||
indent: number;
|
|
||||||
version: number;
|
|
||||||
};
|
|
||||||
[k: string]: unknown;
|
|
||||||
};
|
|
||||||
updatedAt: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "users".
|
|
||||||
*/
|
|
||||||
export interface User {
|
|
||||||
id: number;
|
|
||||||
updatedAt: string;
|
|
||||||
createdAt: string;
|
|
||||||
email: string;
|
|
||||||
resetPasswordToken?: string | null;
|
|
||||||
resetPasswordExpiration?: string | null;
|
|
||||||
salt?: string | null;
|
|
||||||
hash?: string | null;
|
|
||||||
loginAttempts?: number | null;
|
|
||||||
lockUntil?: string | null;
|
|
||||||
sessions?:
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
createdAt?: string | null;
|
|
||||||
expiresAt: string;
|
|
||||||
}[]
|
|
||||||
| null;
|
|
||||||
password?: string | null;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "payload-locked-documents".
|
|
||||||
*/
|
|
||||||
export interface PayloadLockedDocument {
|
|
||||||
id: number;
|
|
||||||
document?:
|
|
||||||
| ({
|
|
||||||
relationTo: 'emails';
|
|
||||||
value: number | Email;
|
|
||||||
} | null)
|
|
||||||
| ({
|
|
||||||
relationTo: 'email-templates';
|
|
||||||
value: number | EmailTemplate;
|
|
||||||
} | null)
|
|
||||||
| ({
|
|
||||||
relationTo: 'users';
|
|
||||||
value: number | User;
|
|
||||||
} | null);
|
|
||||||
globalSlug?: string | null;
|
|
||||||
user: {
|
|
||||||
relationTo: 'users';
|
|
||||||
value: number | User;
|
|
||||||
};
|
|
||||||
updatedAt: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "payload-preferences".
|
|
||||||
*/
|
|
||||||
export interface PayloadPreference {
|
|
||||||
id: number;
|
|
||||||
user: {
|
|
||||||
relationTo: 'users';
|
|
||||||
value: number | User;
|
|
||||||
};
|
|
||||||
key?: string | null;
|
|
||||||
value?:
|
|
||||||
| {
|
|
||||||
[k: string]: unknown;
|
|
||||||
}
|
|
||||||
| unknown[]
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| null;
|
|
||||||
updatedAt: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "payload-migrations".
|
|
||||||
*/
|
|
||||||
export interface PayloadMigration {
|
|
||||||
id: number;
|
|
||||||
name?: string | null;
|
|
||||||
batch?: number | null;
|
|
||||||
updatedAt: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "emails_select".
|
|
||||||
*/
|
|
||||||
export interface EmailsSelect<T extends boolean = true> {
|
|
||||||
template?: T;
|
|
||||||
to?: T;
|
|
||||||
cc?: T;
|
|
||||||
bcc?: T;
|
|
||||||
from?: T;
|
|
||||||
fromName?: T;
|
|
||||||
replyTo?: T;
|
|
||||||
subject?: T;
|
|
||||||
html?: T;
|
|
||||||
text?: T;
|
|
||||||
variables?: T;
|
|
||||||
scheduledAt?: T;
|
|
||||||
sentAt?: T;
|
|
||||||
status?: T;
|
|
||||||
attempts?: T;
|
|
||||||
lastAttemptAt?: T;
|
|
||||||
error?: T;
|
|
||||||
priority?: T;
|
|
||||||
updatedAt?: T;
|
|
||||||
createdAt?: T;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "email-templates_select".
|
|
||||||
*/
|
|
||||||
export interface EmailTemplatesSelect<T extends boolean = true> {
|
|
||||||
name?: T;
|
|
||||||
slug?: T;
|
|
||||||
subject?: T;
|
|
||||||
content?: T;
|
|
||||||
updatedAt?: T;
|
|
||||||
createdAt?: T;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "users_select".
|
|
||||||
*/
|
|
||||||
export interface UsersSelect<T extends boolean = true> {
|
|
||||||
updatedAt?: T;
|
|
||||||
createdAt?: T;
|
|
||||||
email?: T;
|
|
||||||
resetPasswordToken?: T;
|
|
||||||
resetPasswordExpiration?: T;
|
|
||||||
salt?: T;
|
|
||||||
hash?: T;
|
|
||||||
loginAttempts?: T;
|
|
||||||
lockUntil?: T;
|
|
||||||
sessions?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
id?: T;
|
|
||||||
createdAt?: T;
|
|
||||||
expiresAt?: T;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "payload-locked-documents_select".
|
|
||||||
*/
|
|
||||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
|
||||||
document?: T;
|
|
||||||
globalSlug?: T;
|
|
||||||
user?: T;
|
|
||||||
updatedAt?: T;
|
|
||||||
createdAt?: T;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "payload-preferences_select".
|
|
||||||
*/
|
|
||||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
|
||||||
user?: T;
|
|
||||||
key?: T;
|
|
||||||
value?: T;
|
|
||||||
updatedAt?: T;
|
|
||||||
createdAt?: T;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "payload-migrations_select".
|
|
||||||
*/
|
|
||||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
|
||||||
name?: T;
|
|
||||||
batch?: T;
|
|
||||||
updatedAt?: T;
|
|
||||||
createdAt?: T;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "auth".
|
|
||||||
*/
|
|
||||||
export interface Auth {
|
|
||||||
[k: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
declare module 'payload' {
|
|
||||||
export interface GeneratedTypes extends Config {}
|
|
||||||
}
|
|
||||||
@@ -321,9 +321,9 @@ export class MailingService implements IMailingService {
|
|||||||
const email = await this.payload.findByID({
|
const email = await this.payload.findByID({
|
||||||
collection: this.emailsCollection as any,
|
collection: this.emailsCollection as any,
|
||||||
id: emailId,
|
id: emailId,
|
||||||
}) as BaseEmail
|
})
|
||||||
|
|
||||||
const newAttempts = (email.attempts || 0) + 1
|
const newAttempts = ((email as any).attempts || 0) + 1
|
||||||
|
|
||||||
await this.payload.update({
|
await this.payload.update({
|
||||||
collection: this.emailsCollection as any,
|
collection: this.emailsCollection as any,
|
||||||
|
|||||||
Reference in New Issue
Block a user