const fs = require( 'fs-extra' );
const path = require( 'path' );
const formidable = require( 'formidable' );
const jsonfile = require( 'jsonfile' );
const mustache = require( 'mustache' );
const moment = require( 'moment' );
const UUID = require( 'uuid' );
const pdf = require( "pdf-creator-node" );
const nodemailer = require( 'nodemailer' );
const smtpTransport = require( 'nodemailer-smtp-transport' );
const axios = require( 'axios' );
const { GoogleSpreadsheet } = require( 'google-spreadsheet' );
const async = require( 'async' );
const config = require( '../tribes/townconf.js' );
const checkdata = require( `${config.tribes}/${config.mayorId}/www/cdn/public/js/checkdata` );
const Outputs = {};
const sleep = ( milliseconds = 500 ) => new Promise( resolve => setTimeout( resolve, milliseconds ) );
/*
Process any data to a specific output:
emailer => generate a finale text file to send
csv => export json file to csv data
pdf => generate a customized document
*/
Outputs.envoiemail = ( msg, next ) => {
let transporter = nodemailer.createTransport( msg.smtp );
transporter.sendMail( msg, async ( err, info ) => {
if( err ) {
next( err );
} else {
logger.info( 'info', info )
fs.appendFileSync( `${config.tribes}/${msg.headers['x-client-nd-id']}/logs/${msg.headers['x-campaign-id']}_success.txt`, moment( new Date() )
.format( 'YYYYMMDD HH:mm:ss' ) + ' - Success after ' + '0' + ' tries to ' + info.accepted.join( ',' ) + '\n', 'utf-8' );
next( null );
}
} );
};
Outputs.setupmail = ( msg, msg2send, index ) => {
const datacust = {
tribeidperso: msg.tribeidperso,
destperso: index
};
// Evaluation of each field if mustache exist
const datacusteval = {};
Object.keys( datacust.tribeidperso )
.forEach( k => {
if( typeof datacust.tribeidperso[ k ] === 'string' || datacust.tribeidperso[ k ] instanceof String ) {
datacusteval[ k ] = mustache.render( datacust.tribeidperso[ k ], datacust )
} else {
datacusteval[ k ] = datacust.tribeidperso[ k ];
}
} )
Object.keys( datacust.destperso )
.forEach( k => {
if( typeof datacust.destperso[ k ] === 'string' || datacust.destperso[ k ] instanceof String ) {
datacusteval[ k ] = mustache.render( datacust.destperso[ k ], datacust )
} else {
datacusteval[ k ] = datacust.destperso[ k ];
}
} )
msg2send.to = index.email;
logger.info( 'msg2send.to ' + msg2send.to );
msg2send.subject = mustache.render( msg.template.subject, datacusteval );
msg2send.text = mustache.render( msg.template.text, datacusteval );
msg2send.html = mustache.render( msg.template.html, datacusteval );
// TODO need to move that in generemsg
// if (config.emailerurl == 'http://devapia.maildigit.fr:3015') {
// fs.writeFileSync('devdata/tmp/test.html', msg2send.html, 'utf-8');
// logger.info('lancement email sur dev, pour controler le mail générer voir ds ./config.js il faut changer config.emailerurl avec https://mail.maildigit.fr pour envoyer le mail ')
// return {
// status: 200,
// payload: {
// info: ['msgsentok'],
// model: 'Outputs',
// moreinfo: "parametrer sur emailer de dev et pas de production"
// }
// }
// }
return msg2send;
}
Outputs.envoiefirstmail = async ( msg ) => {
logger.info( '###############################################' )
logger.info( "envoie first msg email: " + msg.to )
let transporter = nodemailer.createTransport( msg.smtp );
logger.info( 'attente 1er msg avant d envoyer les autres' );
const transport = await transporter.verify();
logger.info( 'transport', transport );
if( transport.error ) {
logger.info( 'Probleme de smtp', error );
return {
status: 500,
payload: {
info: ''
}
};
} else {
let rep = await transporter.sendMail( msg );
logger.info( 'rep sendMail', rep );
if( rep.accepted && rep.accepted.length > 0 && rep.rejected.length == 0 ) {
fs.appendFileSync( `${config.tribes}/${msg.headers['x-client-nd-id']}/logs/${msg.headers['x-campaign-id']}_success.txt`, moment( new Date() )
.format( 'YYYYMMDD HH:mm:ss' ) + ' - Success after waiting 1st email to send ' + msg.to + '\n', 'utf-8' );
return {
status: '200',
payload: {
info: [ 'send1stemailok' ],
model: 'Outputs'
}
};
}
// status à tester si necessaire de detecter une erreur
// if (rep.rejectedErrors) {
fs.appendFileSync( `${config.tribes}/${msg.headers['x-client-nd-id']}/logs/${msg.headers['x-campaign-id']}_error.txt`, moment( new Date() )
.format( 'YYYYMMDD HH:mm:ss' ) + ' - err after waiting result\n ' + msg.to, 'utf-8' );
return {
status: '500',
payload: {
info: [ 'rejectedError' ],
model: 'Outputs'
}
};
}
}
Outputs.envoiemails = ( msg, msg2send, targets, iteration, resolve, reject ) => {
let newtargets = [];
async.each( targets, ( index, callback ) => { // iterate asynchronously in msg.destperso (targets)
let finalmsg = Outputs.setupmail( msg, msg2send, index );
logger.info( '###############################################' )
logger.info( "envoie msg email: " + finalmsg.to )
Outputs.envoiemail( finalmsg, ( err ) => {
if( err ) { // intentionally don't pass this error in callback, we dont want to break loop
newtargets.push( index ); // stock all errored mails for next try
}
} );
callback();
}, function ( err ) { // part executed only once all iterations are done
if( newtargets.length > 0 && iteration < 4 ) {
setTimeout( () => {
Outputs.envoiemails( msg, msg2send, newtargets, iteration + 1, resolve, reject );
}, 600000 );
} else resolve( newtargets ); // return not resolved errors after 4 trys for log
} );
}
Outputs.generemsg = async ( msg, header ) => {
/*
wait msg sent and return result sent
*/
// Recupere les parametre smtp du domainName à utiliser
logger.info( 'pass Outputs.generemsg' )
try {
const confclientexpediteur = jsonfile.readFileSync( `${config.tribes}/${msg.tribeidperso.tribeidexpediteur}/clientconf.json` );
msg.smtp = confclientexpediteur.smtp;
} catch ( err ) {
logger.info( 'la conf smtp du client n\'est pas definie' );
return {
status: 404,
payload: {
info: [ 'smtpnotdefined' ],
model: 'Outputs'
}
};
}
logger.info( msg );
if( !msg.template.sendascontent && msg.template.htmlfile ) {
try {
msg.template.html = fs.readFileSync( config.sharedData + '/' + msg.template.htmlfile + '/contentinline.mustache', 'utf-8' );
msg.template.text = fs.readFileSync( config.sharedData + '/' + msg.template.htmlfile + '/contenttxt.mustache', 'utf-8' );
} catch ( err ) {
logger.info( 'WARNING, html file template missing ' + config.sharedData + '/' + msg.template.htmlfile );
logger.info( err );
return {
status: 404,
payload: {
info: [ 'fileUnknown' ],
model: 'UploadFiles',
moreinfo: 'Template unavailable, check ' + config.sharedData + '/' + msg.template.htmlfile + '/contentinline.mustache and contenttxt.mustache'
}
};
}
}
if( msg.template.html.length == 0 ) {
logger.info( 'template.html est vide' )
return {
status: 404,
payload: {
info: [ 'ERRnotemplate' ],
model: 'Outputs',
moreinfo: 'No template email check '
}
};
}
// mustache any data into
const msg2send = {};
msg2send.smtp = msg.smtp;
msg2send.from = msg.tribeidperso.from;
if( msg.tribeidperso.cc ) msg2send.cc = msg.tribeidperso.cc;
if( msg.tribeidperso.bcc ) msg2send.bcc = msg.tribeidperso.bcc;
if( msg.tribeidperso.replyTo ) msg2send.replyTo = msg.tribeidperso.replyTo;
msg2send.headers = {
'x-campaign-id': msg.tribeidperso.messageId,
'x-client-nd-id': msg.tribeidperso.tribeid,
'x-template-nd-id': msg.tribeidperso.templateId
};
logger.info( 'nb de message to send:', msg.destperso.length );
// send first mail
const ret = await Outputs.envoiefirstmail( Outputs.setupmail( msg, msg2send, msg.destperso[ 0 ] ) );
logger.info( 'ret 1er msg', ret );
if( ret.status == 200 ) {
pass1ermsg = true;
msg.destperso.shift();
};
logger.info( 'attente 1er msg avant d envoyer les autres' );
// send other mails
new Promise( ( resolve, reject ) => { // useless promise used for recursive calls in Outputs.envoiemails
Outputs.envoiemails( msg, msg2send, msg.destperso, 0, resolve, reject );
} )
.then( ( result ) => {
async.eachSeries( result, ( index, callback ) => { // variante of each but runs only a single operation at a time because of fs.appendFile
fs.appendFile( `${config.tribes}/${msg.headers['x-client-nd-id']}/logs/${msg.headers['x-campaign-id']}_error.txt`, moment( new Date() )
.format( 'YYYYMMDD HH:mm:ss' ) + ' - err after 4 tries to ' + info.rejected.join( ',' ) + '\n', 'utf-8', ( err ) => {
callback( err );
}, ( err ) => {
if( err ) logger.info( err );
} );
logger.info( 'msg.to not well sent', msg.to );
} );
} )
if( pass1ermsg ) {
return {
status: 200,
payload: {
info: [ 'msgsentok' ],
model: 'Outputs'
}
};
} else {
return {
status: 500,
payload: {
info: [ 'msgsentko' ],
model: 'Ouputs',
moreinfo: "1er msg non envoyé car erreur"
}
}
}
};
Outputs.sendMailcampain = async ( msg, headersmsg ) => {
/*
Permet de lancer une campagne de mail personnalisé en mode web service
avec axios config.emailerurl https://mail qui peut être sur un autre serveur que celui en cours
Attention headermsg doit être retraduit avec les champs envoyé par un navigateur
Si le serveur en cours appelle cette fonction les champs du header doivent changer
voir config.js node_env .exposedHeaders
Pour un exemple de msg voir u exemple type de message envoyé dans un tribeid/domain/clientconf.json
avec l'envoi d'email
*/
//logger.info(msg)
// On ajoute le contenu du template directement dans la demande
if( msg.template.sendascontent && msg.template.htmlfile ) {
try {
logger.info( 'test', msg.template.sendascontent )
msg.template.html = fs.readFileSync( config.sharedData + '/' + msg.template.htmlfile + '/contentinline.mustache', 'utf-8' );
msg.template.text = fs.readFileSync( config.sharedData + '/' + msg.template.htmlfile + '/contenttxt.mustache', 'utf-8' );
} catch ( err ) {
logger.info( 'WARNING, html file template missing ' + config.sharedData + '/' + msg.template.htmlfile );
//logger.info(err);
return {
status: 404,
payload: {
info: [ 'fileUnknown' ],
model: 'UploadFiles',
moreinfo: 'Template unavailable, check ' + config.sharedData + '/' + msg.template.htmlfile + '/contentinline.mustache and contenttxt.mustache'
}
};
}
delete msg.template.htmlfile;
if( msg.template.html.length == 0 ) {
return {
status: 404,
payload: {
info: [ 'ERRnotemplate' ],
model: 'Outputs',
moreinfo: 'No template email check '
}
};
}
}
logger.info( 'envoie sur', `${config.emailerurl}/outputs/msg` )
//logger.info(msg)
// on check si les key de headermsg sont des key traduite via exposedHeaders
// (cas ou c'est l'application qui envoie un email)
if( headersmsg.xtribeid ) {
Object.keys( config.exposedHeaders )
.forEach( h => {
headersmsg[ h ] = headersmsg[ config.exposedHeaders[ h ] ];
delete headersmsg[ config.exposedHeaders[ h ] ];
} );
}
// on ajoute le code pour la signature
headersmsg.hashbody = msg.code;
logger.info( 'header after traduction: ', headersmsg )
try {
const resCamp = await axios.post( `${config.emailerurl}/outputs/msg`, msg, {
headers: headersmsg
} );
//logger.info('Etat:', resCamp);
logger.info( 'Tried to send 1st email of the campain ' + msg.destperso[ 0 ].email );
// it just check the 1st email in destperso to return an answer if 1st is ok then all other are send in queue
if( resCamp ) {
return resCamp;
} else {
return { status: 200, payload: { info: [ 'CampainSent' ], model: 'Outputs' } };
}
} catch ( err ) {
// aios error handler
return { status: 401, payload: { info: [ 'erreuraxios' ], model: 'Outputs', moreinfo: err.message } }
}
};
Outputs.get = function ( filename, header ) {
// check file exist
const file = `${config.tribes}/${header.xworkon}/${filename}`;
// logger.info('fichier demande ', file);
if( !fs.existsSync( file ) ) {
// logger.info('le fichier demande n existe pas ', file);
return {
status: 404,
payload: {
info: [ 'fileUnknown' ],
model: 'UploadFiles'
}
};
} else {
logger.info( 'envoie le fichier ', file );
return {
status: 200,
payload: {
info: [ 'fileknown' ],
model: 'UploadFiles',
file: file
}
};
}
};
Outputs.addjson = function ( data, header ) {
/*
Le header = {X-WorkOn:"",destinationfile:"", filename:""}
Le body = {jsonp:{},callback:function to launch after download,'code':'mot cle pour verifier que le fichier est à garder'}
*/
// logger.info(req.body.jsonp);
try {
jsonfile.writeFileSync( header.destinationfile + '/' + header.filename, data.jsonp );
if( data.callback ) {
const execCB = require( `${config.mainDir}/models/tribeid/${header.xworkon
}` );
execCB[ data.callback ]();
}
return {
status: 200,
payload: {
info: [ 'wellUpload' ],
model: 'UploadFiles',
render: {
destination: header.destinationfile,
filename: header.filename
}
}
};
} catch ( err ) {
logger.info( 'Impossible de sauvegarder le fichier, A COMPRENDRE', err );
return {
status: 503,
payload: {
info: [ 'savingError' ],
model: 'UploadFiles'
}
};
}
};
Outputs.add = function ( req, header ) {
const form = new formidable.IncomingForm();
logger.info( 'req.headers', req.headers );
logger.info( 'req.params', req.params );
logger.info( 'req.query', req.query );
logger.info( 'req.body', req.body );
let destinationfile = `${config.tribes}/${header.xworkon}/${header.destinationfile
}`;
form.parse( req, function ( err, fields, files ) {
logger.info( 'files', files.file.path );
logger.info( 'fields', fields );
const oldpath = files.file.path;
destinationfile += '/' + files.file.name;
logger.info( 'oldpath', oldpath );
logger.info( 'destinationfile', destinationfile );
fs.copyFile( oldpath, destinationfile, function ( err ) {
if( err ) {
logger.info( err );
return {
status: 500,
payload: {
info: [ 'savingError' ],
model: 'UploadFiles'
}
};
} else {
logger.info( 'passe' );
fs.unlink( oldpath );
return {
status: 200,
payload: {
info: [ 'wellUpload' ],
model: 'UploadFiles',
render: {
destination: destinationfile
}
}
};
}
} );
} );
};
Outputs.sheettojson = async ( req, header ) => {
doc = new GoogleSpreadsheet( req.productIdSpreadsheet );
await doc.useServiceAccountAuth( req.googleDriveCredentials );
await doc.loadInfo();
let result = [];
for( const sheetName of req.sheets ) {
logger.info( 'loading: ', sheetName );
sheet = doc.sheetsByTitle[ sheetName ]
await sheet.loadHeaderRow();
const records = await sheet.getRows( { offset: 1 } )
records.forEach( record => {
let offer = {}
for( let index = 0; index < record._sheet.headerValues.length; index++ ) {
offer[ record._sheet.headerValues[ index ] ] = record[ record._sheet.headerValues[ index ] ];
}
result.push( offer );
} );
}
return result;
};
Outputs.generepdf = ( req, header ) => {
return new Promise( ( resolve, reject ) => {
let options = {
format: "A4",
orientation: "portrait",
border: "10mm",
footer: {
height: "20mm",
contents: {
default: '{{page}}/{{pages}}', // html pagination if edit needed
}
}
};
let document = {
html: req.html,
data: {
data: req.data,
},
path: `${config.tribes}/${header.xtribeid}/outputs/${UUID.v4()}.pdf`,
type: "",
};
pdf // generate pdf
.create( document, options )
.then( ( res ) => {
resolve( {
status: 200,
payload: {
info: [ 'wellPdfGenerated' ],
model: 'Outputs',
data: {
path: document.path,
filename: req.data.filename
},
render: {
filename: req.data.filename
}
}
} );
} )
.catch( ( err ) => {
reject( {
status: 500,
payload: {
info: [ 'pdfGenerationError' ],
model: 'Outputs',
error: err
}
} );
} );
} );
};
module.exports = Outputs;