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:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user