From 8e3d2b0b914bc3cd32729cb4dc110a6d3c1fbada Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 19 Apr 2023 11:36:28 +0800 Subject: [PATCH] Save epub email attachments --- .../src/{pdf.ts => attachment.ts} | 66 +++++++++++++------ packages/inbound-email-handler/src/index.ts | 25 +++---- 2 files changed, 53 insertions(+), 38 deletions(-) rename packages/inbound-email-handler/src/{pdf.ts => attachment.ts} (58%) diff --git a/packages/inbound-email-handler/src/pdf.ts b/packages/inbound-email-handler/src/attachment.ts similarity index 58% rename from packages/inbound-email-handler/src/pdf.ts rename to packages/inbound-email-handler/src/attachment.ts index 6f60c5d62..273555bae 100644 --- a/packages/inbound-email-handler/src/pdf.ts +++ b/packages/inbound-email-handler/src/attachment.ts @@ -1,41 +1,63 @@ import axios, { AxiosResponse } from 'axios' -import { promisify } from 'util' import * as jwt from 'jsonwebtoken' +import { promisify } from 'util' const signToken = promisify(jwt.sign) +export interface Attachment { + contentType: string + data: Buffer + filename: string | undefined +} + type UploadResponse = { id: string url: string } -export const handlePdfAttachment = async ( +export const isAttachment = (contentType: string, data: Buffer): boolean => { + return ( + (contentType === 'application/pdf' || + contentType === 'application/epub+zip') && + data.length > 0 + ) +} + +export const handleAttachments = async ( email: string, - fileName: string | undefined, - data: Buffer, subject: string, + attachments: Attachment[], receivedEmailId: string ): Promise => { - console.log('handlePdfAttachment', email, fileName) + for await (const attachment of attachments) { + const { contentType, data } = attachment + const filename = + attachment.filename || contentType === 'application/pdf' + ? 'attachment.pdf' + : 'attachment.epub' - fileName = fileName || 'attachment.pdf' - - try { - const uploadResult = await getUploadIdAndSignedUrl(email, fileName) - if (!uploadResult.url || !uploadResult.id) { - console.log('failed to create upload request', uploadResult) - return + try { + const uploadResult = await getUploadIdAndSignedUrl( + email, + filename, + contentType + ) + if (!uploadResult.url || !uploadResult.id) { + console.log('failed to create upload request', uploadResult) + return + } + await uploadToSignedUrl(uploadResult.url, data, contentType) + await createArticle(email, uploadResult.id, subject, receivedEmailId) + } catch (error) { + console.error('handleAttachments error', error) } - await uploadToSignedUrl(uploadResult.url, data) - await createArticle(email, uploadResult.id, subject, receivedEmailId) - } catch (error) { - console.error('handlePdfAttachment error', error) } } const getUploadIdAndSignedUrl = async ( email: string, - fileName: string + fileName: string, + contentType: string ): Promise => { if (process.env.JWT_SECRET === undefined) { throw new Error('JWT_SECRET is not defined') @@ -44,13 +66,14 @@ const getUploadIdAndSignedUrl = async ( const data = { fileName, email, + contentType, } if (process.env.INTERNAL_SVC_ENDPOINT === undefined) { throw new Error('REST_BACKEND_ENDPOINT is not defined') } const response = await axios.post( - `${process.env.INTERNAL_SVC_ENDPOINT}svc/pdf-attachments/upload`, + `${process.env.INTERNAL_SVC_ENDPOINT}svc/email-attachment/upload`, data, { headers: { @@ -64,11 +87,12 @@ const getUploadIdAndSignedUrl = async ( const uploadToSignedUrl = async ( uploadUrl: string, - data: Buffer + data: Buffer, + contentType: string ): Promise => { return axios.put(uploadUrl, data, { headers: { - 'Content-Type': 'application/pdf', + 'Content-Type': contentType, }, maxBodyLength: 1000000000, maxContentLength: 100000000, @@ -97,7 +121,7 @@ const createArticle = async ( throw new Error('REST_BACKEND_ENDPOINT is not defined') } return axios.post( - `${process.env.INTERNAL_SVC_ENDPOINT}svc/pdf-attachments/create-article`, + `${process.env.INTERNAL_SVC_ENDPOINT}svc/email-attachment/create-article`, data, { headers: { diff --git a/packages/inbound-email-handler/src/index.ts b/packages/inbound-email-handler/src/index.ts index 806e1a455..6f88f5ff7 100644 --- a/packages/inbound-email-handler/src/index.ts +++ b/packages/inbound-email-handler/src/index.ts @@ -12,6 +12,7 @@ import * as jwt from 'jsonwebtoken' import parseHeaders from 'parse-headers' import * as multipart from 'parse-multipart-data' import { promisify } from 'util' +import { Attachment, handleAttachments, isAttachment } from './attachment' import { handleGoogleConfirmationEmail, isGoogleConfirmationEmail, @@ -19,7 +20,6 @@ import { parseAuthor, parseUnsubscribe, } from './newsletter' -import { handlePdfAttachment } from './pdf' interface SaveReceivedEmailResponse { id: string @@ -91,17 +91,14 @@ export const inboundEmailHandler = Sentry.GCPFunction.wrapHttpFunction( try { const parts = multipart.parse(req.body, 'xYzZY') const parsed: Record = {} - - let pdfAttachment: Buffer | undefined - let pdfAttachmentName: string | undefined + const attachments: Attachment[] = [] for (const part of parts) { const { name, data, type, filename } = part if (name && data) { parsed[name] = data.toString() - } else if (type === 'application/pdf' && data) { - pdfAttachment = data - pdfAttachmentName = filename + } else if (isAttachment(type, data)) { + attachments.push({ data, contentType: type, filename }) } else { console.log('no data or name for ', part) } @@ -157,16 +154,10 @@ export const inboundEmailHandler = Sentry.GCPFunction.wrapHttpFunction( }) return res.send('ok') } - if (pdfAttachment) { - console.log('handle PDF attachment', from, to) - // save the pdf attachment as an article - await handlePdfAttachment( - to, - pdfAttachmentName, - pdfAttachment, - subject, - receivedEmailId - ) + if (attachments.length > 0) { + console.debug('handle attachments', from, to, subject) + // save the attachments as articles + await handleAttachments(to, subject, attachments, receivedEmailId) return res.send('ok') } // all other emails are considered newsletters