3import React, { useState, useEffect } from 'react';
4import Link from 'next/link';
6interface EmailMessage {
10 processingstatus: number;
11 processingstatus_decoded: string;
12 processingstatus_decoded_bkcolor?: string;
14 messagelength: number;
18interface EmailHeader {
27 contenttransferencoding: string;
30 fdlreaderextract?: string;
31 fdlreadercontents?: string;
32 fdlreaderstatus?: number;
35export default function InboundMessagesPage() {
36 const [messages, setMessages] = useState<EmailMessage[]>([]);
37 const [searchTerm, setSearchTerm] = useState('');
38 const [showNewArrival, setShowNewArrival] = useState(true);
39 const [showReady, setShowReady] = useState(true);
40 const [showDone, setShowDone] = useState(false);
41 const [showDeleting, setShowDeleting] = useState(false);
42 const [loading, setLoading] = useState(true);
45 const [viewModal, setViewModal] = useState(false);
46 const [headersModal, setHeadersModal] = useState(false);
47 const [selectedMessage, setSelectedMessage] = useState<EmailMessage | null>(null);
48 const [messageContent, setMessageContent] = useState('');
49 const [messageHeaders, setMessageHeaders] = useState<EmailHeader[]>([]);
50 const [messageParts, setMessageParts] = useState<EmailPart[]>([]);
51 const [viewedMalware, setViewedMalware] = useState(false);
57 const loadMessages = async () => {
59 const response = await fetch('/api/v1/messaging/inbound');
61 if (!response.ok || !response.headers.get('content-type')?.includes('application/json')) {
63 setMessages(getDemoMessages());
68 const data = await response.json();
69 setMessages(data.messages || []);
72 console.error('Error loading inbound messages:', error);
73 setMessages(getDemoMessages());
78 const getDemoMessages = (): EmailMessage[] => {
82 fromemail: 'customer@example.com',
83 recvdtu: '2025-12-29 14:30:00',
85 processingstatus_decoded: 'Ready',
86 subject: 'Order inquiry - Product availability',
91 fromemail: 'supplier@warehouse.com',
92 recvdtu: '2025-12-29 13:15:00',
94 processingstatus_decoded: 'Completed',
95 subject: 'Invoice #INV-2025-001 attached',
100 fromemail: 'notifications@shipping.com',
101 recvdtu: '2025-12-29 12:45:00',
103 processingstatus_decoded: 'New Arrival',
104 subject: 'Delivery confirmation for order #12345',
110 const getStatusBadgeColor = (status: number) => {
112 case 0: return 'bg-blue-100 text-blue-800';
113 case 1: return 'bg-green-100 text-green-800';
114 case 2: return 'bg-red-100 text-red-800';
115 case 4: return 'bg-gray-100 text-gray-800';
118 return 'bg-orange-500 text-white';
119 default: return 'bg-gray-100 text-gray-800';
123 const decodeStatus = (status: number): { text: string; bgColor?: string } => {
125 case 0: return { text: 'New Arrival' };
126 case 1: return { text: 'Ready' };
127 case 2: return { text: 'Deleting' };
128 case 4: return { text: 'Done' };
129 case 256: return { text: '***MALWARE DETECTED***', bgColor: 'orange' };
130 case 257: return { text: '***ATTACHMENT MALWARE DETECTED***', bgColor: 'orange' };
131 default: return { text: `Unknown (${status})` };
135 const filteredMessages = messages.filter(msg => {
137 const statusMap: { [key: number]: boolean } = {
144 if (statusMap[msg.processingstatus] === false) return false;
148 const search = searchTerm.toLowerCase();
150 msg.fromemail.toLowerCase().includes(search) ||
151 msg.subject.toLowerCase().includes(search)
158 const handleViewMessage = async (msg: EmailMessage) => {
160 if ((msg.processingstatus >= 256 && msg.processingstatus < 512) && !viewedMalware) {
161 if (!confirm("This message has failed antivirus checks. It could be dangerous to your computer.\n\nDo you really wish to view it?")) {
164 if (!confirm("Are you really sure you want to continue?\n\nIt has been recorded that you have viewed this against advice.\n\nSupport due to damage may be chargeable")) {
167 setViewedMalware(true);
170 setSelectedMessage(msg);
171 setMessageContent('Loading message content...');
175 // Fetch message content and parts
177 const response = await fetch(`/api/v1/messaging/inbound/${msg.physkey}/content`);
179 if (!response.ok || !response.headers.get('content-type')?.includes('application/json')) {
181 setMessageContent(`Demo message content for: ${msg.subject}\n\nThis is a sample email body. In production, the actual email content would be displayed here.\n\nFrom: ${msg.fromemail}\nReceived: ${msg.recvdtu}`);
185 contenttype: 'text/html',
186 contenttransferencoding: 'quoted-printable',
187 name: 'message.html',
188 partdata: 'Demo HTML content',
192 contenttype: 'application/pdf',
193 contenttransferencoding: 'base64',
195 partdata: 'base64encodeddata...',
196 fdlreaderextract: '{"invoice": "data"}',
202 const data = await response.json();
203 setMessageContent(data.content || 'No content available');
204 setMessageParts(data.parts || []);
206 console.error('Error loading message content:', error);
207 setMessageContent('Error loading message content');
211 const handleViewHeaders = async (msg: EmailMessage) => {
212 setSelectedMessage(msg);
213 setMessageHeaders([]);
214 setHeadersModal(true);
217 const response = await fetch(`/api/v1/messaging/inbound/${msg.physkey}/headers`);
219 if (!response.ok || !response.headers.get('content-type')?.includes('application/json')) {
222 { name: 'From', value: msg.fromemail, seq: 1 },
223 { name: 'To', value: 'store@yourcompany.com', seq: 2 },
224 { name: 'Subject', value: msg.subject, seq: 3 },
225 { name: 'Date', value: msg.recvdtu, seq: 4 },
226 { name: 'Content-Type', value: 'multipart/mixed; boundary="boundary123"', seq: 5 },
227 { name: 'MIME-Version', value: '1.0', seq: 6 },
232 const data = await response.json();
233 setMessageHeaders(data.headers || []);
235 console.error('Error loading headers:', error);
239 const handleReprocess = async (msg: EmailMessage, event: React.MouseEvent) => {
240 const ctrlKey = event.ctrlKey;
243 if (!confirm('Reprocess ALL messages? This will reprocess every message in the list.')) {
249 const response = await fetch('/api/v1/messaging/inbound/reprocess', {
251 headers: { 'Content-Type': 'application/json' },
252 body: JSON.stringify({
253 physkey: ctrlKey ? 'all' : msg.physkey,
258 alert('Reprocess queued (demo mode)');
262 alert('Message reprocessing queued');
265 console.error('Error reprocessing:', error);
266 alert('Reprocess queued (demo mode)');
270 const handleDelete = async (msg: EmailMessage) => {
271 if (!confirm(`Delete message from ${msg.fromemail}?\n\n${msg.subject}`)) {
276 const response = await fetch(`/api/v1/messaging/inbound/${msg.physkey}`, {
281 alert('Delete queued (demo mode)');
285 alert('Message deletion queued');
288 console.error('Error deleting:', error);
289 alert('Delete queued (demo mode)');
293 const formatSize = (bytes: number): string => {
294 return ((bytes + 512) / 1024).toFixed(2) + 'kb';
298 <div className="container mx-auto px-4 py-8">
299 <div className="mb-6">
300 <div className="flex items-center gap-4 mb-4">
301 <Link href="/marketing/messaging" className="text-blue-600 hover:text-blue-800">
304 <h1 className="text-3xl font-bold text-gray-900">Inbound Messages</h1>
307 <p className="text-gray-600 mb-4">
308 This page lists emails that have arrived and their automatic processing status.
309 Inbound email processing must be specifically configured and is typically only used
310 where Fieldpine has its own email account.
314 {/* Search and Filters */}
315 <div className="bg-white rounded-lg shadow-md p-6 mb-6">
316 <div className="flex flex-wrap items-center gap-4">
317 <div className="flex-1 min-w-[200px]">
318 <label className="block text-sm font-medium text-gray-700 mb-2">
319 Search (from/subject)
324 onChange={(e) => setSearchTerm(e.target.value)}
325 placeholder="Filter by sender or subject..."
326 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
330 <div className="flex flex-wrap items-center gap-4">
331 <span className="text-sm font-medium text-gray-700">Show:</span>
333 <label className="flex items-center gap-2 cursor-pointer">
336 checked={showNewArrival}
337 onChange={(e) => setShowNewArrival(e.target.checked)}
338 className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
340 <span className="text-sm text-gray-700">New Arrival</span>
343 <label className="flex items-center gap-2 cursor-pointer">
347 onChange={(e) => setShowReady(e.target.checked)}
348 className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
350 <span className="text-sm text-gray-700">Ready</span>
353 <label className="flex items-center gap-2 cursor-pointer">
357 onChange={(e) => setShowDone(e.target.checked)}
358 className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
360 <span className="text-sm text-gray-700">Done</span>
363 <label className="flex items-center gap-2 cursor-pointer">
366 checked={showDeleting}
367 onChange={(e) => setShowDeleting(e.target.checked)}
368 className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
370 <span className="text-sm text-gray-700">Deleting</span>
376 {/* Messages Table */}
377 <div className="bg-white rounded-lg shadow-md overflow-hidden">
378 <div className="overflow-x-auto">
379 <table className="min-w-full divide-y divide-gray-200">
380 <thead className="bg-gray-50">
382 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
385 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
388 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
391 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
394 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
397 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
402 <tbody className="bg-white divide-y divide-gray-200">
405 <td colSpan={6} className="px-6 py-8 text-center text-gray-500">
409 ) : filteredMessages.length === 0 ? (
411 <td colSpan={6} className="px-6 py-8 text-center text-gray-500">
412 No messages found matching the current filters.
416 filteredMessages.map((msg, index) => (
417 <tr key={msg.physkey} className="hover:bg-gray-50">
418 <td className="px-6 py-4 whitespace-nowrap">
419 <div className="text-sm text-gray-900 max-w-xs truncate" title={msg.fromemail}>
423 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
426 <td className="px-6 py-4 whitespace-nowrap">
427 <span className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusBadgeColor(msg.processingstatus)}`}>
428 {msg.processingstatus_decoded}
431 <td className="px-6 py-4 whitespace-nowrap text-sm space-x-2">
433 onClick={() => handleViewMessage(msg)}
434 className="text-blue-600 hover:text-blue-900 font-medium"
439 onClick={() => handleViewHeaders(msg)}
440 className="text-blue-600 hover:text-blue-900 font-medium"
445 onClick={(e) => handleReprocess(msg, e)}
446 className="text-green-600 hover:text-green-900 font-medium"
447 title="Hold Ctrl to reprocess all"
451 {msg.processingstatus !== 2 && (
453 onClick={() => handleDelete(msg)}
454 className="text-red-600 hover:text-red-900 font-medium"
460 <td className="px-6 py-4">
461 <div className="text-sm text-gray-900 max-w-md truncate" title={msg.subject}>
465 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
466 {formatSize(msg.messagelength)}
476 {/* View Message Modal */}
477 {viewModal && selectedMessage && (
479 className="modal-overlay"
480 onClick={() => setViewModal(false)}
483 className="bg-white rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] overflow-hidden"
484 onClick={(e) => e.stopPropagation()}
486 <div className="border-b border-gray-200 px-6 py-4 flex items-center justify-between">
487 <h2 className="text-xl font-semibold text-gray-900">
489 <span className="text-sm text-gray-500 ml-3">({selectedMessage.physkey})</span>
492 onClick={() => setViewModal(false)}
493 className="text-gray-400 hover:text-gray-600 text-2xl font-bold"
499 <div className="p-6 overflow-y-auto max-h-[calc(90vh-120px)]">
500 {/* Message Parts */}
501 {messageParts.length > 0 && (
502 <div className="mb-6">
503 <h3 className="text-lg font-semibold mb-3">Attachments & Parts</h3>
504 <table className="min-w-full border border-gray-300">
505 <thead className="bg-gray-50">
507 <th className="px-4 py-2 border-b text-left text-sm font-medium text-gray-700">Content Type</th>
508 <th className="px-4 py-2 border-b text-left text-sm font-medium text-gray-700">Encoding</th>
509 <th className="px-4 py-2 border-b text-left text-sm font-medium text-gray-700">Name</th>
510 <th className="px-4 py-2 border-b text-left text-sm font-medium text-gray-700">Options</th>
514 {messageParts.map((part, idx) => (
515 <tr key={idx} className="hover:bg-gray-50">
516 <td className="px-4 py-2 border-b text-sm">{part.contenttype}</td>
517 <td className="px-4 py-2 border-b text-sm">{part.contenttransferencoding}</td>
518 <td className="px-4 py-2 border-b text-sm">{part.name || '-'}</td>
519 <td className="px-4 py-2 border-b text-sm">
520 {part.fdlreaderextract && (
522 onClick={() => alert(`Extract data:\n${part.fdlreaderextract}`)}
523 className="text-blue-600 hover:text-blue-800 text-sm mr-2"
528 {part.fdlreadercontents && (
530 onClick={() => alert(`Contents:\n${part.fdlreadercontents}`)}
531 className="text-blue-600 hover:text-blue-800 text-sm"
544 {/* Message Content */}
545 <div className="mb-4">
546 <h3 className="text-lg font-semibold mb-3">Message Content</h3>
547 <pre className="bg-gray-50 p-4 rounded border border-gray-300 overflow-x-auto text-sm whitespace-pre-wrap">
553 <div className="border-t border-gray-200 px-6 py-4 flex justify-end">
555 onClick={() => setViewModal(false)}
556 className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
565 {/* View Headers Modal */}
566 {headersModal && selectedMessage && (
568 className="modal-overlay"
569 onClick={() => setHeadersModal(false)}
572 className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden"
573 onClick={(e) => e.stopPropagation()}
575 <div className="border-b border-gray-200 px-6 py-4 flex items-center justify-between">
576 <h2 className="text-xl font-semibold text-gray-900">
578 <span className="text-sm text-gray-500 ml-3">({selectedMessage.physkey})</span>
581 onClick={() => setHeadersModal(false)}
582 className="text-gray-400 hover:text-gray-600 text-2xl font-bold"
588 <div className="p-6 overflow-y-auto max-h-[calc(90vh-120px)]">
589 <table className="min-w-full border border-gray-300">
590 <thead className="bg-gray-50">
592 <th className="px-4 py-2 border-b text-left text-sm font-medium text-gray-700">Name</th>
593 <th className="px-4 py-2 border-b text-left text-sm font-medium text-gray-700">Value</th>
597 {messageHeaders.map((header, idx) => (
598 <tr key={idx} className="hover:bg-gray-50">
599 <td className="px-4 py-2 border-b text-sm font-medium">{header.name}</td>
600 <td className="px-4 py-2 border-b text-sm break-all">{header.value}</td>
607 <div className="border-t border-gray-200 px-6 py-4 flex justify-end">
609 onClick={() => setHeadersModal(false)}
610 className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"