Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Post Angular form data to Node.js with Sendgrid/Nodemailer

I have followed this example to post data from my Angular app to Node.js to post a webform to Sendgrid. This works fine after some changes and thanks a lot for the quickstart. Posting my form data to Sendgrid is working now!

For this project i'm using Angular Fullstack to be able to use Node functionalities within my Angular app.

However, this example has just input fields and a textarea. I want to be able to add a file (PDF, Docx, e.g) so that people can send an attachment to the recipient via Sendgrid. I have searched for a solution but couldn't find a working example. Maybe because it is not possible what i want to achieve.

MY VIEW (CLIENT):

<div ng-controller="ContactFormCtrl" id="contactformbox" style="margin-top:50px;" class="mainbox" >                    
  <div class="panel panel-info" >

          <div class="panel-heading">
              <div class="panel-title">Solliciteer direct</div>
          </div>     

          <div style="padding-top:30px" class="panel-body" >
              <form id="loginform" class="form-horizontal" role="form" name="contactform">

                  <div style="margin-bottom: 25px" class="input-group">
                    <span class="input-group-addon"><i class="glyphicon glyphicon-user"></i></span>
                    <input type="email" name="to" ng-model="email.to" ng-required="true" id="email-to" class="form-control" name="username" value="" placeholder="The emailadres from the employer">       
                  </div>

                  <div style="margin-bottom: 25px" class="input-group">
                      <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i></span>
                      <input type="email" name="from" ng-model="email.from" ng-required="true" id="email-from" class="form-control" name="email-from" placeholder="Your e-mail address">
                  </div>

                  <div style="margin-bottom: 25px" class="input-group">
                      <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i></span>
                      <input type="text" name="subject" ng-model="email.subject" ng-required="true" id="email-subject" class="form-control" name="subject" placeholder="Your subject please">
                  </div>

                  <div style="margin-bottom: 25px" class="input-group">
                      <input type="file" name="file" ng-model="email.file" ng-required="true" id="email-file" class="form-control" name="file">
                  </div>

                  <div style="margin-bottom: 25px" class="input-group">
                    <textarea ng-model="email.text" name="text" placeholder="Enter Text Here.." class="form-control" rows="5" id="comment"></textarea>
                  </div>    

                  <div style="margin-top:10px" class="form-group">
                      <!-- Button -->
                      <div class="col-sm-12 controls">
                            <button id="emailSubmitBn" class="btn btn-success" type="submit" ng-click="submitEmail()">Submit</button>
                      </div>
                  </div>
              </form>     
        </div>                     
  </div>  

MY CONTROLLER (CLIENT):

angular.module('angularMyApp')
.controller('ContactFormCtrl', function ($scope, $http) {
  $scope.submitEmail = function() {

    console.log("TEST");
    //Request
    $http.post('/api/email', $scope.email) 
    .success(function(data, status) {
        console.log("Sent ok");
    })
    .error(function(data, status) {
        console.log("Error");
    })
  };
});

MY APP.JS (SERVER):

'use strict';

// Set default node environment to development
process.env.NODE_ENV = process.env.NODE_ENV || 'development';

var express = require('express');
var config = require('./config/environment');
var http = require('http');
var bodyParser = require('body-parser');

var options = {
    auth: {
        api_key: process.env.SENDGRID_APIKEY; 
    }
}

var nodemailer = require('nodemailer');
var sgTransport = require('nodemailer-sendgrid-transport');

// Setup server
var app = express();
var server = require('http').createServer(app);
require('./config/express')(app);
require('./routes')(app);

var mailer = nodemailer.createTransport(sgTransport(options));

app.post('/api/email', function(req, res) {
var mailOptions = {
    to: ['[email protected]', req.body.to],
    from: req.body.from,
    subject: req.body.subject,
    text: req.body.text
};

mailer.sendMail(mailOptions, function(err, res) {
    if (err) { 
        console.log(err) 
    }
    console.log(res);
  });
});

// Start server
server.listen(config.port, config.ip, function () {
console.log('Express server listening on %d, in %s mode', config.port, app.get('env'));
});

// Expose app
exports = module.exports = app;

Well there are two main issues:

  1. Clientside: How can i post the attachment from Angular to Node within this form? Do i have to upload the file first or can i send it to Node with $http.post? Or do i have to use ng-file-upload?
  2. Serverside: How can i send an attachment to Sendgrid/Nodemailer. Sending a hardcoded file from my app.js on the server to Sendgrid doesn't work. The mail is send succesfully to Sendgrid but it doesn't contain an attachment.

Many thanks in advance!

like image 457
Simon Janssen Avatar asked Sep 02 '15 13:09

Simon Janssen


2 Answers

1. Uploading a file with a form in angular

The first trouble you have is that ng-model doesn't work with <input type="file" />. So, you will need to create a custom directive to populate the model with the file.

.directive('fileModel', ['$parse', function ($parse) {
    return {
        restrict: 'A',
        link: function(scope, element, attrs) {
            var model = $parse(attrs.fileModel);
            var modelSetter = model.assign;

            element.bind('change', function(){
                scope.$apply(function(){
                    modelSetter(scope, element[0].files[0]);
                });
            });
        }
    };
}]);

Then use the directive on your file input element like this:

<input type="file" file-model="email.attachment" ng-required="true" id="email-attachment" name="attachment" class="form-control" />

Note that I changed email.file to email.attachment to avoid confusion in the rest of the code.

Next, you need to include the file in your AJAX request. You will need to use FormData for that. Populate it from the scope using FormData.append():

$scope.submitEmail = function() {
    var formData = new FormData();
    Object.keys($scope.email).forEach(function(key) {
        formData.append(key, $scope.email[key]);
    });
    $http.post('/api/email', formData, {
        transformRequest: angular.identity,
        headers: {'Content-Type': undefined}
    }).then(function(data, status) {
        console.log("Sent ok");
    }, function(data, status) {
        console.log("Error");
    });
};

Notice I also passed a config object to $http.post(). This is to prevent angular from parsing the FormData object and setting the content type.

I relied heavily on this blog post for this part of the answer.

2. Sending an attachment with Nodemailer

To access the file in express, use multer.

Install:

$ npm install --save multer

Usage:

var multer = require('multer');
var upload = multer();

app.post('/api/email', upload.single('attachment'), function(req, res) {
    // req.file contains the 'attachment' file
    ...
});

From Nodemailer's Readme section on email fields it says to use an attachments property that is an array of attachment objects.

app.post('/api/email', upload.single('attachment'), function(req, res) {
    var mailOptions = {
        to: ['[email protected]', req.body.to],
        from: req.body.from,
        subject: req.body.subject,
        text: req.body.text,
        attachments: [
            {
                filename: req.file.originalname,
                content: req.file.buffer
            }
        ]
    };

    mailer.sendMail(mailOptions, function(err, res) {
        if (err) { 
            console.log(err) 
        }
        console.log(res);
    });
});

The above example keeps the attachment in memory, which could be bad if large files are uploaded frequently. You can write files to disk instead:

var upload = multer({ dest: '/uploads' });

Then, instead of setting the file's buffer as the attachment's content, you would set the attachment's path to the file's path:

attachments: [
    {
        filename: req.file.originalname,
        path: req.file.path
    }
]
like image 54
gilly3 Avatar answered Sep 20 '22 19:09

gilly3


I finally got the job done thanks to gilly3. I had to do one minor change in his code. In the controller I changed:

formData.set(key, $scope.email[key]);

to the following code:

formData.append(key, $scope.email[key]);
like image 23
Simon Janssen Avatar answered Sep 21 '22 19:09

Simon Janssen