Folding JSON strings to send in header variables - Mailgun - javascript

I'm trying to use the JS Mailgun API to send emails. Have it working fine, until I throw template variables into 'h:X-Mailgun-Variables' like so, where jsonString is very large (17000+ characters):
const mailData = {
from: 'Insights <insights#hello.net>',
to: mailAddress,
subject: `Insights: ${DAYS_OF_WEEK[date.getDay()]}, ${MONTHS[date.getMonth()]} ${ordinal_suffix_of(date.getDate())} ${date.getFullYear()}`,
template: "template1",
'h:X-Mailgun-Variables': jsonString,
};
Looking at the documentation here states the following:
Note The value of the “X-Mailgun-Variables” header must be valid JSON string,
otherwise Mailgun won’t be able to parse it. If your X-Mailgun-Variables
header exceeds 998 characters, you should use folding to spread the variables
over multiple lines.
Referenced this post, which suggested I "fold" up the JSON by inserting CRLF characters at regular intervals. This led me here, which still does not work, though logging this does show regular line breaks and is compliant JSON:
const jsonString = JSON.stringify(templateVars).split('},').join('},\r \n');
Any insight into how to properly "fold" my JSON so I can use large template variables in my MailGun emails?
UPDATE:
As requested, adding my code. This works when data only has a few companies/posts, but when I have many companies each with many posts, I get the 400 error:
function dispatchEmails(data) {
const DOMAIN = 'test.net';
const mg = mailgun({apiKey: API_KEY, domain: DOMAIN});
const templateVars = {
date: theDate,
previewText: 'preview',
subject: 'subject',
subhead: 'subhead',
companies: data.companies.map(company => {
return {
url: company.url,
totalParts: data.totalParts,
currentPart: data.currentPart,
companyData: {
name: company.name,
website: company.website,
description: company.description
},
posts: _.map(company.news, item => {
return {
category: item.category,
date: new Date(item.date),
url: item.sourceUrl,
title: item.title,
source: item.publisherName,
description: item.description,
}
})
}
})
};
const jsonString = JSON.stringify(templateVars).split('},').join('},\r \n');
const mailData = {
from: 'test#test.com',
to: 'recipient#test.com',
subject: 'subject',
template: 'template',
'h:X-Mailgun-Variables': jsonString
};
return mg.messages().send(mailData)
.then(body => {
return body;
})
.catch(err => {
return {error: err};
});
}

I think your problem may be overall payload size rather than the string folding. The folding for strings exceeding 998 characters seems to be handled by the node.js client, possibly by the form-data package.
I ran the following test:
test_big_data (template created in mailgun through the UI)
<!DOCTYPE html>
<html>
<body>
<h2>An HTML template for testing large data sets.</h2>
<ul>
<li>{{param_name_0}}</li>
... other lis ...
<li>{{param_name_999}}</li>
</ul>
</body>
</html>
send_email.js
const API_KEY = 'MY_KEY';
const DOMAIN = 'MY_DOMAIN.COM';
const formData = require('form-data');
const Mailgun = require('mailgun.js');
const mailgun = new Mailgun(formData);
const client = mailgun.client({ username: 'api', key: API_KEY });
const bigData = {};
for (let i = 0; i < 400; i++) {
bigData[`param_name_${i}`] = `param_value_${i}`;
}
const dataString = JSON.stringify(bigData);
console.log(dataString.length);
const messageData = {
from: 'Mailgun Sandbox <postmaster#DOMAIN>',
to: 'test#MY_DOMAIN.COM',
subject: 'Big Data Test',
template: 'test_big_data',
'h:X-Mailgun-Variables': dataString,
};
client.messages
.create(DOMAIN, messageData)
.then((res) => {
console.log(res);
})
.catch((err) => {
console.error(err);
});
The length of the dataString in this case is 13781 characters and the email queues successfully.
However, if I bump the for loop condition to i < 1000 I get the following error when queueing the email:
[Error: Bad Request] {
status: 400,
details: '{"message":"Send options (parameters starting with o:, h:, or v:) are limited to 16 kB total"}\n'
}
When I asked Mailgun support about the folding warning form the documentation they pointed me to RFC 2822 section "3.2.3. Folding white space and comments". But like I said, I don't think folding is the issue here.
Cheers!
https://datatracker.ietf.org/doc/html/rfc2822#page-11

Just thinking outside the box but why pass that much data in an email header? I assume you have something on the receiving end which is going to parse the email headers. What if instead of sending them that data you send them a key that they can call back into an API on your end to get the data

Related

HubspotClient - Update contact by email id is not working

In NodeJS, I'm using "#hubspot/api-client": "^7.1.2".
Created hubspot client using accessToken as follows
const hubSpotClient = new hubspot.Client({ accessToken });
When I try to update the contact using email it's throwing error
Request:
const idProperty = 'email';
const response = await hubSpotClient(store).crm.contacts.basicApi.update(email, idProperty, contact);
Response:
ERROR {
"statusCode": 404,
"body": {
"status": "error",
"message": "Object not found. objectId are usually numeric.",
"correlationId": "71e911d3...",
"context": {
"id": [
"testemail#..."
]
},
"category": "OBJECT_NOT_FOUND"
}
Create contact is working fine with this client but updating by email is not working.
Anything out of place or syntax error in passing the idProperty?
The problem is in your implementation, because it seems like you are not using properly the Hubspot API.
If you check the function signature of the basicApi.update
public async update(contactId: string, simplePublicObjectInput: SimplePublicObjectInput, idProperty?: string, _options?: Configuration): Promise<RequestContext> {
Basically, you need to pass down a contactId, and then a simplePublicObjectInput that is basically an object that represents your update.
Your code should look like this:
import { Client } from "#hubspot/api-client";
const hubspotClient = new Client({ accessToken: YOUR_ACCESS_TOKEN });
const contactID = 1234;
const response = await hubspotClient.crm.contacts.basicApi.update(contactID, {
properties: { email: 'my-new-email#gmail.com' },
})
Keep in mind that Hubspot always tries to follow their same guidelines as their endpoints. If your check the endpoint specification you will see the following:
Think about the Hubspot node client as just an abstraction of some http client, but at the end does exactly the same as the endpoints described in their implementations.
For that reason, in your implementation, Hubspot is returning an appropriated error, since you are not giving the contactId in the first argument, Hubspot is telling you: "Object not found. objectId are usually numeric." Because indeed a Contact ID is numeric and you are using the value of an email --string-- instead.
If you want to "update by email"
I think that there is no direct way to do it, you might need to do a previous search of the contact by email.
You could use the searchApi.
And after getting the id just run the update.
const searchResponse = await hubspotClient.crm.contacts.searchApi.doSearch({
filterGroups: [
{
filters: [
{
value: 'email-to-search#gmail.com',
propertyName: 'email',
operator: 'EQ',
},
],
},
],
sorts: [],
properties: [],
limit: 1,
after: 0,
});
// Off course you need to improve a lot the error handling here and so on.
// This is just an example
const [contactID] = searchResponse.results;
const contactUpdateResponse = await hubspotClient.crm.contacts.basicApi.update(contactID, {
properties: { email: 'my-new-email#gmail.com' },
})
I hope this helps you!
You CAN use email as the idProperty for the hubspot/api-client get contact function, but it only works if you fill in all the other query fields before idProperty, even if they are undefined.
Here is my example of a getContactByEmail as a Google Cloud Function in Node, using the api-client, and it works great!
exports.getContactByEmail = functions.https.onCall(async (data, context) => {
const email = data.email;
const contactId = email;
const properties = ["firstname", "lastname", "company"];
const propertiesWithHistory = undefined;
const associations = undefined;
const archived = false;
const idProperty = "email";
try {
const apiResponse = await hubspotClient.crm.contacts.basicApi.getById(
contactId,
properties,
propertiesWithHistory,
associations,
archived,
idProperty
);
console.log(JSON.stringify(apiResponse.body, null, 2));
return apiResponse.properties;
} catch (error) {
error.message === "HTTP request failed"
? console.error(JSON.stringify(error.response, null, 2))
: console.error(error);
return error;
}
});

JS HTTP post YAML

I am unable to post a yaml throught http in js. The code works when I used JSON.stringify(text) for body and have content-type application/json. If I use YAML stringify, body on server side is just {}.
I have areatext where I enter a text (like yaml format) in html e.g.
martin:
name: Martin D'vloper
job: Developer
skill: Elite
Client:
$('#create-yaml2').on('click',async function () {
var text = $('#yaml-create').val();
// console.log(text)
var textYAML = YAML.stringify(text);
var options = {
hostname: 'myhostname',
port: 80,
path: `/api/postyaml`,
method: 'POST',
body: textYAML,
headers: {
'Content-Type': 'text/x-yaml'
}
};
var executeReq = http.request(options);
executeReq.write(textYAML);
executeReq.end();
});
EDIT:
Function in server side, that prints not an empty {} when posting JSON.
exports.functionCalled = async function (req, res) {
console.log('\n\n\n\n')
console.log(req.body)
console.log('\n\n\n\n')
try {
res.send(`RECEIVED`);
} catch (upgradeErr) {
res.status(422).json({
message: `Failed`,
error: upgradeErr
});
}
}
You already have a string coming from HTML, so you don't need to call YAML.stringify once again - text is already a string.
$('#create-yaml2').on('click',async function () {
var text = $('#yaml-create').val();
// console.log(text)
var textYAML = text;
var options = {
hostname: 'myhostname',
port: 80,
path: `/api/postyaml`,
method: 'POST',
body: textYAML,
headers: {
'Content-Type': 'text/x-yaml'
}
};
var executeReq = http.request(options);
executeReq.write(textYAML);
executeReq.end();
});
You may want to do something like
$('#create-yaml2').on('click',async function () {
var text = $('#yaml-create').val();
try {
YAML.parse(text)
} catch(e) {...}
...
send request
to make sure there was a valid YAML provided
YAML.stringify converts a JavaScript data structure to a string containing YML.
You don't have a JavaScript data structure, you just a string containing YML.
Almost. You have an error in it. You can't use a raw ' in a YML string that isn't quoted.
So:
Fix your YML:
martin:
name: "Martin D'vloper"
job: Developer
skill: Elite
Don't double encode it:
var textYAML = $('#yaml-create').val();

Axios get URL not working after concatenation

I am having a problem using Axios with node js. Here is my code
let callResult = await axios.get(urlData, config)
Where config is
let config = {
headers: {
'X-Token': token
}
};
And the urlData is
let urlData = 'https://api.regulaforensics.com/webapi/Transaction2/GetTransactionResultJson?transactionId=<IDVariable>&resultType=15'
I am trying to add my IDVariable to the URL but it does not work. If I take the variable and put it directly in the URL I get a response.
I have also tried this
let config = {
headers: {
'X-Token': token
},
params: {
transactionId: IDVariable,
resultType: 15
}};
And this
let querys = querystring.stringify({ transactionId: keyId, resultType: 15 })
let path = 'https://api.regulaforensics.com/webapi/Transaction2/GetTransactionResultJson?'
let urlData = path.concat("", querys)
This is the complete URL
https://api.regulaforensics.com/webapi/Transaction2/GetTransactionResultJson?transactionId=05cc6ccc-3ae6-4185-b2c9-1e1aba01d705&resultType=15
When using {params: } or concatenation
When putting the whole URL. As the URL I pasted above
This is my whole function
If you want to pass some parameters as a query string you can use the following syntax for the GET request:
axios.get('/user', { params: { ID: 12345 } });
Tath will be translated in the following request:
/user?ID=12345
As explained in the documentation here: https://github.com/axios/axios#note-commonjs-usage
Note
You don't need to add the ? char at the end of URL and you don't need to have the parameters in the URL, so the part ?transactionId=<IDVariable>&resultType=15 must be removed
I found the solution.
I am calling a service first and the response of that service is the key I need for the second service.
The solution was to just put a sleep() between those two services.
Thanks you for your response, guys!

How to Login to MediaWiki (Wikipedia) API in Node.js

I'm trying the Wikipedia client login flow depicted in the API:Login docs, but something wrong happens:
1) I correctly get a token raised with the HTTP GET https://en.wikipedia.org/w/api.php?action=query&meta=tokens&type=login&format=json
and I get a valid logintoken string.
2.1) I then try the clientlogin like:
HTTP POST /w/api.php?action=clientlogin&format=json&lgname=xxxx&lgtoken=xxxx%2B%5C
and the POST BODY was
{
"lgpassword" : "xxxxx",
"lgtoken" : "xxxxx"
}
But I get an error:
{
"error": {
"code": "notoken",
"info": "The \"token\" parameter must be set."
},
"servedby": "mw1228"
}
If I try to change lgtoken to token I get the same result.
2.2) I have then tried the old method i.e. action=login and passing the body, but it does not work, since it gives me back another login token: HTTP POST https://en.wikipedia.org/w/api.php?action=login&format=json&lgname=xxxx
and the same POST BODY
I then get
{
"warnings": {}
},
"login": {
"result": "NeedToken",
"token": "xxxxx+\\"
}
where the docs here states that
NeedToken if the lgtoken parameter was not provided or no session was active (e.g. your cookie handling is broken).
but I have passed the lgtoken in the json body as showed.
I'm using Node.js and the built-in http module, that is supposed to pass and keep session Cookies in the right way (with other api it works ok).
I have found a similar issue on a the LrMediaWiki client here.
[UPDATE]
This is my current implementation:
Wikipedia.prototype.loginUser = function (username, password) {
var self = this;
return new Promise((resolve, reject) => {
var cookies = self.cookies({});
var headers = {
'Cookie': cookies.join(';'),
'Accept': '*/*',
'User-Agent': self.browser.userAgent()
};
// fetch login token
self.api.RequestGetP('/w/api.php', headers, {
action: 'query',
meta: 'tokens',
type: 'login',
format: 'json'
})
.then(response => { // success
if (response.query && response.query.tokens && response.query.tokens['logintoken']) {
self.login.logintoken = response.query.tokens['logintoken'];
self.logger.info("Wikipedia.login token:%s", self.login);
return self.api.RequestPostP('/w/api.php', headers, {
action: 'login',
format: 'json',
lgname: username
},
{
lgpassword: password,
lgtoken: self.login.logintoken
});
} else {
var error = new Error('no logintoken');
return reject(error);
}
})
.then(response => { // success
return resolve(response);
})
.catch(error => { // error
self.logger.error("Wikipedia.login error%s\n%#", error.message, error.stack);
return reject(error);
});
});
}//loginUser
where this.api is a simple wrapper of the Node.js http, the source code is available here and the api signatures are like:
Promise:API.RequestGetP(url,headers,querystring)
Promise:API.RequestPostP(url,headers,querystring,body)
If the currently accepted answer isn't working for someone, the following method will definitely work. I've used the axios library to send requests. Any library can be used but the key lies in formatting the body and headers correctly.
let url = "https://test.wikipedia.org/w/api.php";
let params = {
action: "query",
meta: "tokens",
type: "login",
format: "json"
};
axios.get(url, { params: params }).then(resp => {
let loginToken = resp.data.query.tokens.logintoken
let cookie = resp.headers["set-cookie"].join(';');
let body = {
action: 'login',
lgname: 'user_name',
lgpassword: 'password',
lgtoken: loginToken,
format: 'json'
}
let bodyData = new URLSearchParams(body).toString();
axios.post(url, bodyData, {
headers: {
Cookie: cookie,
}
}).then(resp => {
// You're now logged in!
// You'll have to add the following cookie in the headers again for any further requests that you might make
let cookie = resp.headers["set-cookie"].join(';')
console.log(resp.data)
})
})
And you should be seeing a response like
{
login: { result: 'Success', lguserid: 0000000, lgusername: 'Username' }
}
The second post request was where I got stuck for several hours, trying to figure out what was wrong. You need to send the data in an encoded form by using an API like URLSearchParams, or by just typing up the body as a string manually yourself.
I think from what you are saying you have lgtoken and lgname in the URL you are using, and then lgpassword and lgtoken (again!) in a JSON-encoded POST body.
This is not how the Mediawiki API works.
You submit it all as POST parameters. JSON is never involved, except when you ask for the result to come back in that format. I can't help you fix your code as you don't provide it, but that's what you need to do. (If you edit your question with your code, I'll do my best to help you.)
After seeing your code, I'll presume (without knowing the detail of your code) that you want something like this:
return self.api.RequestPostP('/w/api.php', headers, {
action: 'login',
format: 'json',
lgname: username,
lgpassword: password,
lgtoken: self.login.logintoken
});

Is it possible to create a meeting request for outlook with nodeJS?

I am developing a very basic calendar with Angular and Node and I haven't found any code on this.
Workflow is the following : create an event, input the recipient's e-mail address, validate the event.
This triggers an e-mail sent to the recipient. The mail should be in the outlook meeting request format (not an attached object).
This means that when received in outlook the meeting is automatically added in the calendar.
Is this possible? If yes is it possible with only javascript on Node side?
For those still looking for an answer, here's how I managed to get the perfect solution for me.
I used iCalToolkit to create a calendar object.
It's important to make sure all the relevant fields are set up (organizer and attendees with RSVP).
Initially I was using the Postmark API service to send my emails but this solution was only working by sending an ics.file attachment.
I switched to the Postmark SMTP service where you can embed the iCal data inside the message and for that I used nodemailer.
This is what it looks like :
var icalToolkit = require('ical-toolkit');
var postmark = require('postmark');
var client = new postmark.Client('xxxxxxxKeyxxxxxxxxxxxx');
var nodemailer = require('nodemailer');
var smtpTransport = require('nodemailer-smtp-transport');
//Create a iCal object
var builder = icalToolkit.createIcsFileBuilder();
builder.method = meeting.method;
//Add the event data
var icsFileContent = builder.toString();
var smtpOptions = {
host:'smtp.postmarkapp.com',
port: 2525,
secureConnection: true,
auth:{
user:'xxxxxxxKeyxxxxxxxxxxxx',
pass:'xxxxxxxPassxxxxxxxxxxx'
}
};
var transporter = nodemailer.createTransport(smtpTransport(smtpOptions));
var mailOptions = {
from: 'message#domain.com',
to: meeting.events[0].attendees[i].email,
subject: 'Meeting to attend',
html: "Anything here",
text: "Anything here",
alternatives: [{
contentType: 'text/calendar; charset="utf-8"; method=REQUEST',
content: icsFileContent.toString()
}]
};
//send mail with defined transport object
transporter.sendMail(mailOptions, function(error, info){
if(error){
console.log(error);
}
else{
console.log('Message sent: ' + info.response);
}
});
This sends a real meeting request with the Accept, decline and Reject button.
It's really unbelievable the amount of work you need to go through for such a trivial functionality and how all of this not well documented.
Hope this helps.
If you do not want to use smtp server approach in earlier accepted solution, you have Exchange focused solution available. Whats wrong in current accepted answer? it does not create a meeting in sender's Calendar, you do not have ownership of the meeting item for further modification by the sender Outlook/OWA.
here is code snippet in javascript using npm package ews-javascript-api
var ews = require("ews-javascript-api");
var credentials = require("../credentials");
ews.EwsLogging.DebugLogEnabled = false;
var exch = new ews.ExchangeService(ews.ExchangeVersion.Exchange2013);
exch.Credentials = new ews.ExchangeCredentials(credentials.userName, credentials.password);
exch.Url = new ews.Uri("https://outlook.office365.com/Ews/Exchange.asmx");
var appointment = new ews.Appointment(exch);
appointment.Subject = "Dentist Appointment";
appointment.Body = new ews.TextBody("The appointment is with Dr. Smith.");
appointment.Start = new ews.DateTime("20170502T130000");
appointment.End = appointment.Start.Add(1, "h");
appointment.Location = "Conf Room";
appointment.RequiredAttendees.Add("user1#constoso.com");
appointment.RequiredAttendees.Add("user2#constoso.com");
appointment.OptionalAttendees.Add("user3#constoso.com");
appointment.Save(ews.SendInvitationsMode.SendToAllAndSaveCopy).then(function () {
console.log("done - check email");
}, function (error) {
console.log(error);
});
Instead of using 'ical-generator', I used 'ical-toolkit' to build a calender invite event.
Using this, the invite directly gets appended in the email instead of the attached .ics file object.
Here is a sample code:
const icalToolkit = require("ical-toolkit");
//Create a builder
var builder = icalToolkit.createIcsFileBuilder();
builder.method = "REQUEST"; // The method of the request that you want, could be REQUEST, PUBLISH, etc
//Add events
builder.events.push({
start: new Date(2020, 09, 28, 10, 30),
end: new Date(2020, 09, 28, 12, 30),
timestamp: new Date(),
summary: "My Event",
uid: uuidv4(), // a random UUID
categories: [{ name: "MEETING" }],
attendees: [
{
rsvp: true,
name: "Akarsh ****",
email: "Akarsh **** <akarsh.***#abc.com>"
},
{
rsvp: true,
name: "**** RANA",
email: "**** RANA <****.rana1#abc.com>"
}
],
organizer: {
name: "A****a N****i",
email: "A****a N****i <a****a.r.n****i#abc.com>"
}
});
//Try to build
var icsFileContent = builder.toString();
//Check if there was an error (Only required if yu configured to return error, else error will be thrown.)
if (icsFileContent instanceof Error) {
console.log("Returned Error, you can also configure to throw errors!");
//handle error
}
var mailOptions = { // Set the values you want. In the alternative section, set the calender file
from: obj.from,
to: obj.to,
cc: obj.cc,
subject: result.email.subject,
html: result.email.html,
text: result.email.text,
alternatives: [
{
contentType: 'text/calendar; charset="utf-8"; method=REQUEST',
content: icsFileContent.toString()
}
]
}
//send mail with defined transport object
transporter.sendMail(mailOptions, function(error, info){
if(error){
console.log(error);
}
else{
console.log('Message sent: ' + info.response);
}
});
It should be possible as long as you can use SOAP in Node and also if you can use NTLM authentication for Exchange with Node. I believe there are modules for each.
I found this blog very helpful when designing a similar system using PHP
Please check the following sample:
const options = {
authProvider,
};
const client = Client.init(options);
const onlineMeeting = {
startDateTime: '2019-07-12T14:30:34.2444915-07:00',
endDateTime: '2019-07-12T15:00:34.2464912-07:00',
subject: 'User Token Meeting'
};
await client.api('/me/onlineMeetings')
.post(onlineMeeting);
More Information: https://learn.microsoft.com/en-us/graph/api/application-post-onlinemeetings?view=graph-rest-1.0&tabs=http

Categories