Search code examples
node.jstypescriptamazon-s3nodemailer

Sending email attachments hosted in S3 via Nodemailer using ReadableStreams


I'm trying to send emails with Nodemailer containing attachments hosted in S3, using JS AWS SDK v3. The Nodemailer docs show an example of sending an attachment using a read stream generated from reading a file:

{   // stream as an attachment
    filename: 'text4.txt',
    content: fs.createReadStream('file.txt')
},

I can create a ReadableStream for my attachment in S3 like so:

const s3Stream = await s3Client
    .send(/* ... */)
    .then((response) => {
        // ...
        return response.Body.transformToWebStream() // returns Promise<ReadableStream>
    })
    .catch(/* ... */)

If I attempt to pass s3Stream directly into the content field for my Nodemailer attachment, I get an error: Type 'ReadableStream' is not assignable to type 'string | Readable | Buffer | undefined.

The Readable class has a static method fromWeb. Calling Readable.fromWeb(s3Stream) gives the following:

Argument of type 'ReadableStream' is not assignable to parameter of type 'ReadableStream<any>'.
  Type 'ReadableStream' is missing the following properties from type 'ReadableStream<any>': locked, cancel, getReader, pipeThrough, and 4 more.

Readable objects have a method wrap:

new Readable().wrap(s3Stream)

Trying this results in the following error:

Argument of type 'ReadableStream' is not assignable to parameter of type 'NodeJS.ReadableStream'.
  Type 'ReadableStream' is missing the following properties from type 'ReadableStream': readable, read, setEncoding, pause, and 22 more.

Solution

  • That's a known problem with S3 SDK (check out this GitHub issue), it shows response.Body as a union of typings for both browsers (ReadableStream) and node.js (Readable), while at the runtime returning a determined type of value depending on the platform you run it on.

    So if you are sure that your code will only work in node.js you just do type casting:

    import { Readable } from 'node:stream';
    
    (res.Body as Readable)?.pipe(...);
    

    Or even better add a type guard just to be sure:

    import { Readable } from 'node:stream';
    
    if(isReadable(res.Body)) {
        res.Body.pipe(fs.createWriteStream('./file2.txt'));
    } else {
        throw new TypeError('res.Body is not a readable stream');
    }
    
    function isReadable(value: unknown): value is Readable {
      return value instanceof Readable
    }