Skip to main content

Overview

Inbound makes it easy to work with email attachments. This guide covers sending attachments, receiving them via webhooks, and downloading attachment files.

Sending Attachments

Basic Attachment

import { Inbound } from 'inboundemail'
import { readFileSync } from 'fs'

const inbound = new Inbound(process.env.INBOUND_API_KEY)

// Read file and encode to base64
const pdfContent = readFileSync('./invoice.pdf').toString('base64')

const email = await inbound.emails.send({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'Invoice #12345',
  html: '<p>Please find your invoice attached.</p>',
  attachments: [
    {
      filename: 'invoice-12345.pdf',
      content: pdfContent,
      content_type: 'application/pdf'
    }
  ]
})

Multiple Attachments

const email = await inbound.emails.send({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'Monthly Report',
  html: '<p>Monthly report with charts attached.</p>',
  attachments: [
    {
      filename: 'report.pdf',
      content: pdfBase64,
      content_type: 'application/pdf'
    },
    {
      filename: 'chart.png',
      content: imageBase64,
      content_type: 'image/png'
    },
    {
      filename: 'data.csv',
      content: csvBase64,
      content_type: 'text/csv'
    }
  ]
})

Inline Images

Embed images directly in HTML:
const email = await inbound.emails.send({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'Weekly Newsletter',
  html: `
    <div>
      <h1>This Week's Featured Product</h1>
      <img src="cid:product-image" alt="Product" />
      <p>Check out our amazing new product!</p>
    </div>
  `,
  attachments: [
    {
      filename: 'product.jpg',
      content: imageBase64,
      content_type: 'image/jpeg',
      content_id: 'product-image' // Reference in HTML with cid:product-image
    }
  ]
})

Common Content Types

const contentTypes = {
  // Documents
  'pdf': 'application/pdf',
  'doc': 'application/msword',
  'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  'xls': 'application/vnd.ms-excel',
  'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  'ppt': 'application/vnd.ms-powerpoint',
  'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  
  // Images
  'jpg': 'image/jpeg',
  'jpeg': 'image/jpeg',
  'png': 'image/png',
  'gif': 'image/gif',
  'svg': 'image/svg+xml',
  'webp': 'image/webp',
  
  // Text/Data
  'txt': 'text/plain',
  'csv': 'text/csv',
  'json': 'application/json',
  'xml': 'application/xml',
  
  // Archives
  'zip': 'application/zip',
  'tar': 'application/x-tar',
  'gz': 'application/gzip',
  
  // Other
  'mp4': 'video/mp4',
  'mp3': 'audio/mpeg',
  'ics': 'text/calendar'
}

Receiving Attachments

Webhook Payload

When an email with attachments arrives, the webhook payload includes attachment metadata:
{
  "type": "email.received",
  "email": {
    "id": "email_abc123",
    "subject": "Document submission",
    "from": { "addresses": [{ "address": "[email protected]" }] },
    "attachments": [
      {
        "filename": "resume.pdf",
        "content_type": "application/pdf",
        "size": 245678,
        "content_id": null
      },
      {
        "filename": "cover-letter.docx",
        "content_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "size": 89234,
        "content_id": null
      }
    ]
  }
}

Processing Attachments in Webhook

import { type InboundWebhookPayload, isInboundWebhookPayload } from 'inboundemail'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const payload = await request.json()
  
  if (!isInboundWebhookPayload(payload)) {
    return NextResponse.json({ error: 'Invalid webhook' }, { status: 400 })
  }
  
  const { email } = payload
  
  // Check for attachments
  if (email.attachments && email.attachments.length > 0) {
    console.log(`📎 Email has ${email.attachments.length} attachment(s)`)
    
    for (const attachment of email.attachments) {
      console.log('Processing attachment:', {
        filename: attachment.filename,
        type: attachment.content_type,
        size: `${(attachment.size / 1024).toFixed(2)} KB`
      })
      
      // Download attachment
      await downloadAndProcessAttachment(email.id, attachment.filename)
    }
  }
  
  return NextResponse.json({ success: true })
}

Downloading Attachments

Download attachment files using the Inbound API:

Using the SDK

import { Inbound } from 'inboundemail'
import { writeFileSync } from 'fs'

const inbound = new Inbound(process.env.INBOUND_API_KEY)

async function downloadAttachment(emailId: string, filename: string) {
  try {
    // Get attachment as buffer
    const buffer = await inbound.attachments.download(emailId, filename)
    
    // Save to disk
    writeFileSync(`./downloads/${filename}`, buffer)
    
    console.log(`Downloaded: ${filename}`)
  } catch (error) {
    console.error('Download failed:', error)
  }
}

// Usage
await downloadAttachment('email_abc123', 'resume.pdf')

Using Fetch API

async function downloadAttachment(emailId: string, filename: string) {
  const url = `https://inbound.new/api/e2/attachments/${emailId}/${encodeURIComponent(filename)}`
  
  const response = await fetch(url, {
    headers: {
      'Authorization': `Bearer ${process.env.INBOUND_API_KEY}`
    }
  })
  
  if (!response.ok) {
    throw new Error(`Download failed: ${response.statusText}`)
  }
  
  const buffer = await response.arrayBuffer()
  return Buffer.from(buffer)
}

Upload to Cloud Storage

AWS S3

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { Inbound } from 'inboundemail'

const s3 = new S3Client({ region: 'us-east-1' })
const inbound = new Inbound(process.env.INBOUND_API_KEY)

async function uploadAttachmentToS3(
  emailId: string,
  filename: string,
  contentType: string
) {
  // Download from Inbound
  const buffer = await inbound.attachments.download(emailId, filename)
  
  // Upload to S3
  const key = `attachments/${emailId}/${filename}`
  
  await s3.send(new PutObjectCommand({
    Bucket: 'my-bucket',
    Key: key,
    Body: buffer,
    ContentType: contentType
  }))
  
  const url = `https://my-bucket.s3.amazonaws.com/${key}`
  console.log(`Uploaded to S3: ${url}`)
  
  return url
}

Cloudinary

import { v2 as cloudinary } from 'cloudinary'

cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET
})

async function uploadToCloudinary(emailId: string, filename: string) {
  // Download attachment
  const buffer = await inbound.attachments.download(emailId, filename)
  const base64 = buffer.toString('base64')
  
  // Upload to Cloudinary
  const result = await cloudinary.uploader.upload(
    `data:application/octet-stream;base64,${base64}`,
    {
      public_id: `email-attachments/${emailId}/${filename}`,
      resource_type: 'auto'
    }
  )
  
  console.log('Uploaded to Cloudinary:', result.secure_url)
  return result.secure_url
}

Attachment Size Limits

Maximum attachment size: 10 MB per fileTotal size for all attachments in a single email: 25 MB
Handle oversized attachments:
const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024 // 10 MB

async function sendEmailWithLargeFiles(files: Array<{ path: string, name: string }>) {
  const attachments = []
  const skippedFiles = []
  
  for (const file of files) {
    const stats = statSync(file.path)
    
    if (stats.size > MAX_ATTACHMENT_SIZE) {
      // Upload to cloud storage instead
      const url = await uploadToS3(file.path, file.name)
      skippedFiles.push({ name: file.name, url })
    } else {
      // Include as attachment
      const content = readFileSync(file.path).toString('base64')
      attachments.push({
        filename: file.name,
        content,
        content_type: getMimeType(file.name)
      })
    }
  }
  
  // Build email with links to large files
  let html = '<p>Files attached to this email:</p><ul>'
  
  attachments.forEach(att => {
    html += `<li>${att.filename} (attached)</li>`
  })
  
  skippedFiles.forEach(file => {
    html += `<li><a href="${file.url}">${file.name}</a> (too large, download link)</li>`
  })
  
  html += '</ul>'
  
  await inbound.emails.send({
    from: '[email protected]',
    to: '[email protected]',
    subject: 'Files Ready',
    html,
    attachments
  })
}

Security Best Practices

Validate File Types

const ALLOWED_TYPES = [
  'application/pdf',
  'image/jpeg',
  'image/png',
  'application/msword',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
]

function validateAttachment(attachment: any) {
  // Check content type
  if (!ALLOWED_TYPES.includes(attachment.content_type)) {
    throw new Error(`File type not allowed: ${attachment.content_type}`)
  }
  
  // Check file extension
  const ext = attachment.filename.split('.').pop()?.toLowerCase()
  const allowedExtensions = ['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']
  
  if (!ext || !allowedExtensions.includes(ext)) {
    throw new Error(`File extension not allowed: ${ext}`)
  }
  
  // Check size
  const maxSize = 5 * 1024 * 1024 // 5 MB
  if (attachment.size > maxSize) {
    throw new Error(`File too large: ${attachment.size} bytes`)
  }
}

Scan for Viruses

import { ClamScan } from 'clamscan'

const clamscan = await new ClamScan().init({
  clamdscan: {
    socket: '/var/run/clamav/clamd.sock',
    timeout: 60000
  }
})

async function scanAttachment(buffer: Buffer) {
  const { isInfected, viruses } = await clamscan.scanBuffer(buffer)
  
  if (isInfected) {
    console.error('Virus detected:', viruses)
    throw new Error(`Virus detected: ${viruses.join(', ')}`)
  }
  
  console.log('File is clean')
}

Sanitize Filenames

function sanitizeFilename(filename: string): string {
  return filename
    .replace(/[^a-z0-9._-]/gi, '_')  // Remove special chars
    .replace(/_{2,}/g, '_')           // Remove multiple underscores
    .substring(0, 255)                // Limit length
}

// Usage
const safeFilename = sanitizeFilename('../../etc/passwd')  // "etc_passwd"

Complete Example: Document Processing

import { Inbound, type InboundWebhookPayload } from 'inboundemail'
import { NextRequest, NextResponse } from 'next/server'
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'

const inbound = new Inbound(process.env.INBOUND_API_KEY!)
const s3 = new S3Client({ region: 'us-east-1' })

const ALLOWED_TYPES = [
  'application/pdf',
  'application/msword',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
]

export async function POST(request: NextRequest) {
  const payload: InboundWebhookPayload = await request.json()
  const { email } = payload
  
  if (!email.attachments || email.attachments.length === 0) {
    return NextResponse.json({ success: true, message: 'No attachments' })
  }
  
  console.log(`Processing ${email.attachments.length} attachment(s)...`)
  
  const processedFiles = []
  
  for (const attachment of email.attachments) {
    try {
      // Validate
      if (!ALLOWED_TYPES.includes(attachment.content_type)) {
        console.warn(`Skipping unsupported file: ${attachment.filename}`)
        continue
      }
      
      if (attachment.size > 10 * 1024 * 1024) {
        console.warn(`File too large: ${attachment.filename}`)
        continue
      }
      
      // Download
      console.log(`Downloading: ${attachment.filename}`)
      const buffer = await inbound.attachments.download(email.id, attachment.filename)
      
      // Upload to S3
      const key = `documents/${email.id}/${sanitizeFilename(attachment.filename)}`
      
      await s3.send(new PutObjectCommand({
        Bucket: 'my-document-bucket',
        Key: key,
        Body: buffer,
        ContentType: attachment.content_type,
        Metadata: {
          'email-id': email.id,
          'sender': email.from?.addresses?.[0]?.address || 'unknown',
          'original-filename': attachment.filename
        }
      }))
      
      const url = `https://my-document-bucket.s3.amazonaws.com/${key}`
      
      processedFiles.push({
        filename: attachment.filename,
        url,
        size: attachment.size
      })
      
      console.log(`✅ Processed: ${attachment.filename}`)
      
    } catch (error) {
      console.error(`Failed to process ${attachment.filename}:`, error)
    }
  }
  
  // Send confirmation
  if (processedFiles.length > 0) {
    await inbound.reply(email, {
      from: '[email protected]',
      html: `
        <p>We've received your ${processedFiles.length} document(s):</p>
        <ul>
          ${processedFiles.map(f => `<li>${f.filename}</li>`).join('')}
        </ul>
        <p>Thanks!</p>
      `
    })
  }
  
  return NextResponse.json({
    success: true,
    processed: processedFiles.length
  })
}

function sanitizeFilename(filename: string): string {
  return filename.replace(/[^a-z0-9._-]/gi, '_').substring(0, 255)
}

Troubleshooting

Attachment Not Found

try {
  const buffer = await inbound.attachments.download(emailId, filename)
} catch (error) {
  if (error.status === 404) {
    console.error('Attachment not found. Check:')
    console.error('1. Email ID is correct')
    console.error('2. Filename matches exactly (case-sensitive)')
    console.error('3. Filename is URL-encoded if it contains spaces')
  }
}

Large Downloads Timing Out

// Increase timeout for large files
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 60000) // 60 seconds

try {
  const response = await fetch(url, {
    headers: { 'Authorization': `Bearer ${apiKey}` },
    signal: controller.signal
  })
  
  const buffer = await response.arrayBuffer()
  clearTimeout(timeout)
  
  return Buffer.from(buffer)
} catch (error) {
  if (error.name === 'AbortError') {
    console.error('Download timeout')
  }
  throw error
}

Next Steps

Sending Emails

Learn more about sending emails

Receiving Emails

Handle incoming emails with webhooks

Replying

Reply with attachments

API Reference

View attachments API docs

Build docs developers (and LLMs) love