fix: Rewrite JMAP attachment upload to use HTTP POST

- Use proper HTTP POST to upload blob to JMAP server
- Truemail JMAP requires /upload/{accountId}/ endpoint
- Simplified email creation with correct bodyStructure
- Better error logging for debugging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2026-01-30 08:45:50 +01:00
parent 9bc8e2084a
commit a97a84b4f9

View File

@@ -1,5 +1,6 @@
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import axios from 'axios';
import { db } from '../../config/database.js'; import { db } from '../../config/database.js';
import { prilohy, registracie, ucastnici, kurzy, emailAccounts, userEmailAccounts } from '../../db/schema.js'; import { prilohy, registracie, ucastnici, kurzy, emailAccounts, userEmailAccounts } from '../../db/schema.js';
import { eq, and } from 'drizzle-orm'; import { eq, and } from 'drizzle-orm';
@@ -152,6 +153,35 @@ V prípade otázok nás neváhajte kontaktovať.
`.trim(); `.trim();
}; };
/**
* Upload blob via HTTP POST to JMAP upload endpoint
*/
const uploadBlobToJmap = async (jmapConfig, fileBuffer, mimeType) => {
const jmapServer = process.env.JMAP_SERVER || 'https://mail.truemail.sk/jmap/';
// Truemail upload URL format
const uploadUrl = `${jmapServer}upload/${jmapConfig.accountId}/`;
logger.info(`Nahrávam súbor na JMAP server: ${uploadUrl}`);
const response = await axios.post(uploadUrl, fileBuffer, {
auth: {
username: jmapConfig.username,
password: jmapConfig.password,
},
headers: {
'Content-Type': mimeType,
},
});
if (!response.data?.blobId) {
logger.error('Upload nevrátil blobId', response.data);
throw new Error('Nepodarilo sa nahrať prílohu na server');
}
logger.info(`Súbor nahraný, blobId: ${response.data.blobId}`);
return response.data.blobId;
};
/** /**
* Send email with HTML content and PDF attachment via JMAP * Send email with HTML content and PDF attachment via JMAP
*/ */
@@ -166,11 +196,14 @@ const sendEmailWithAttachment = async (jmapConfig, to, subject, htmlBody, textBo
throw new Error('Priečinok Odoslané nebol nájdený'); throw new Error('Priečinok Odoslané nebol nájdený');
} }
// Read attachment file and encode to base64 // Read attachment file
const fileBuffer = await fs.readFile(attachment.path); const fileBuffer = await fs.readFile(attachment.path);
const base64Content = fileBuffer.toString('base64'); const mimeType = attachment.mimeType || 'application/pdf';
// Create email with attachment // Upload blob via HTTP POST (this is the correct JMAP way)
const blobId = await uploadBlobToJmap(jmapConfig, fileBuffer, mimeType);
// Create email with uploaded blob
const createResponse = await jmapRequest(jmapConfig, [ const createResponse = await jmapRequest(jmapConfig, [
[ [
'Email/set', 'Email/set',
@@ -181,29 +214,36 @@ const sendEmailWithAttachment = async (jmapConfig, to, subject, htmlBody, textBo
mailboxIds: { mailboxIds: {
[sentMailbox.id]: true, [sentMailbox.id]: true,
}, },
keywords: {
$draft: true,
},
from: [{ email: jmapConfig.username }], from: [{ email: jmapConfig.username }],
to: [{ email: to }], to: [{ email: to }],
subject: subject, subject: subject,
htmlBody: [{ partId: 'html', type: 'text/html' }], bodyStructure: {
textBody: [{ partId: 'text', type: 'text/plain' }], type: 'multipart/mixed',
attachments: [ subParts: [
{ {
blobId: null, // Will be set via bodyValues type: 'multipart/alternative',
type: attachment.mimeType || 'application/pdf', subParts: [
name: attachment.filename, {
disposition: 'attachment', type: 'text/plain',
}, partId: 'text',
], },
{
type: 'text/html',
partId: 'html',
},
],
},
{
type: mimeType,
name: attachment.filename,
disposition: 'attachment',
blobId: blobId,
},
],
},
bodyValues: { bodyValues: {
html: { html: { value: htmlBody },
value: htmlBody, text: { value: textBody },
},
text: {
value: textBody,
},
}, },
}, },
}, },
@@ -212,148 +252,12 @@ const sendEmailWithAttachment = async (jmapConfig, to, subject, htmlBody, textBo
], ],
]); ]);
// Check if we need to upload blob first (for some JMAP servers)
const createResult = createResponse.methodResponses[0][1]; const createResult = createResponse.methodResponses[0][1];
if (createResult.notCreated?.draft) {
// Try alternative approach - upload blob first, then create email
logger.info('Skúšam alternatívny prístup s nahraním prílohy');
// Upload blob
const uploadResponse = await jmapRequest(jmapConfig, [
[
'Blob/upload',
{
accountId: jmapConfig.accountId,
create: {
attachment1: {
data: [{ 'data:asBase64': base64Content }],
type: attachment.mimeType || 'application/pdf',
},
},
},
'blob1',
],
]);
const blobId = uploadResponse.methodResponses[0][1]?.created?.attachment1?.blobId;
if (!blobId) {
// Try yet another approach - inline base64
logger.info('Skúšam inline base64 prílohu');
const inlineResponse = await jmapRequest(jmapConfig, [
[
'Email/set',
{
accountId: jmapConfig.accountId,
create: {
draft: {
mailboxIds: {
[sentMailbox.id]: true,
},
keywords: {
$draft: true,
},
from: [{ email: jmapConfig.username }],
to: [{ email: to }],
subject: subject,
bodyStructure: {
type: 'multipart/mixed',
subParts: [
{
type: 'multipart/alternative',
subParts: [
{
type: 'text/plain',
partId: 'text',
},
{
type: 'text/html',
partId: 'html',
},
],
},
{
type: attachment.mimeType || 'application/pdf',
name: attachment.filename,
disposition: 'attachment',
partId: 'attachment',
},
],
},
bodyValues: {
html: { value: htmlBody },
text: { value: textBody },
attachment: { value: base64Content, isEncodingProblem: false },
},
},
},
},
'set2',
],
]);
const inlineResult = inlineResponse.methodResponses[0][1];
const createdEmailId = inlineResult.created?.draft?.id;
if (!createdEmailId) {
logger.error('Nepodarilo sa vytvoriť email s prílohou', inlineResult.notCreated?.draft);
throw new Error('Nepodarilo sa vytvoriť email s prílohou');
}
// Submit the email
return await submitEmail(jmapConfig, createdEmailId);
}
// Create email with uploaded blob
const emailWithBlobResponse = await jmapRequest(jmapConfig, [
[
'Email/set',
{
accountId: jmapConfig.accountId,
create: {
draft: {
mailboxIds: {
[sentMailbox.id]: true,
},
from: [{ email: jmapConfig.username }],
to: [{ email: to }],
subject: subject,
htmlBody: [{ partId: 'html', type: 'text/html' }],
textBody: [{ partId: 'text', type: 'text/plain' }],
attachments: [
{
blobId: blobId,
type: attachment.mimeType || 'application/pdf',
name: attachment.filename,
disposition: 'attachment',
},
],
bodyValues: {
html: { value: htmlBody },
text: { value: textBody },
},
},
},
},
'set3',
],
]);
const createdEmailId = emailWithBlobResponse.methodResponses[0][1].created?.draft?.id;
if (!createdEmailId) {
throw new Error('Nepodarilo sa vytvoriť email s prílohou');
}
return await submitEmail(jmapConfig, createdEmailId);
}
const createdEmailId = createResult.created?.draft?.id; const createdEmailId = createResult.created?.draft?.id;
if (!createdEmailId) { if (!createdEmailId) {
throw new Error('Nepodarilo sa vytvoriť email'); logger.error('Nepodarilo sa vytvoriť email s prílohou', createResult.notCreated?.draft);
throw new Error('Nepodarilo sa vytvoriť email s prílohou');
} }
return await submitEmail(jmapConfig, createdEmailId); return await submitEmail(jmapConfig, createdEmailId);