Skip to content

Instantly share code, notes, and snippets.

@sebastienvercammen
Last active March 9, 2022 09:58
Show Gist options
  • Save sebastienvercammen/e7e0e9e57db246d7f941b789d8508186 to your computer and use it in GitHub Desktop.
Save sebastienvercammen/e7e0e9e57db246d7f941b789d8508186 to your computer and use it in GitHub Desktop.
Nintendo PTC Account Verifier for Gmail v2 with CORS proxy (via Google Scripts & jQuery)
/*
Automatically click all "Verify your email" links in the welcome e-mail from
Nintendo Pokémon Trainer Club's signup e-mails.
Only unread emails in inbox will be processed.
All processed e-mails will be marked as read if verification was successful,
and optionally moved to trash if it's enabled in your settings.
How to use:
1. Login to Gmail
2. Go to https://script.google.com/
3. Enter the code in Code.gs and index.html (if index.html doesn't exist, create it).
4. Edit the settings at the top of index.html.
5. In the editor, click on Resources > Advanced Google services and name your project.
6. Enable "These services must also be enabled in the Google Developers Console".
7. Enable GMail API for your new project in the Google Developers Console: https://console.developers.google.com/.
8. Click Publish, then run as web app.
9. You'll see the results on the page. Dev console can be used for debugging.
10. Enjoy
*/
// Serve HTML file.
function doGet() {
return HtmlService.createHtmlOutputFromFile('index.html');
}
function getAllEmailLinks(limit) {
// Prepare response.
var items = [];
// Start.
var threads = GmailApp.search('in:inbox -label:RM_EXPIRED -label:RM_PREACTIVATED is:unread subject:"Pokémon Trainer Club Activation"', 0, limit);
Logger.log("Found " + threads.length + " threads.");
threads.forEach(function (thread) {
var messages = thread.getMessages();
Logger.log("Found " + messages.length + " messages.");
messages.forEach(function (msg) {
// If we have a limit, follow it.
if (limit > 0 && items.length >= limit) {
return;
}
var value = msg.getBody()
.match(/verify your email/m);
if (msg.isInInbox() && value) {
var messageId = msg.getId();
var link = msg.getBody().match(/<a href="https:\/\/club.pokemon.com\/us\/pokemon-trainer-club\/activated\/([\w\d]+)"/);
if (link) {
var url = 'https://club.pokemon.com/us/pokemon-trainer-club/activated/' + link[1];
// Add to list for front-end.
items.push({
'url': url,
'id': messageId
});
}
}
});
});
Logger.log("Sent " + items.length + " URLs to front-end for verification.");
return items;
}
function labelAsError(messageId, labelName) {
try {
modifyMessage('me', messageId, [ labelName ]);
return {
'success': true
};
} catch (e) {
Logger.log(e);
return {
'success': false,
'error': e
};
}
}
function markAsVerified(messageId, move_to_trash) {
var msg = GmailApp.getMessageById(messageId);
msg.markRead();
if (move_to_trash) {
msg.moveToTrash();
}
}
/*
* Below methods are Google Apps Script Gmail Utilities, by mogsdad:
* https://gist.github.com/mogsdad/6515581
*/
/**
* Modify the Labels a Message is associated with.
* Throws if unsuccessful.
* see https://developers.google.com/gmail/api/v1/reference/users/messages/modify
*
* @param {String} userId User's email address. The special value 'me'
* can be used to indicate the authenticated user.
* @param {String} messageId ID of Message to modify.
* @param {String} labelsToAdd Array of Label names to add.
* @param {String} labelsToRemove Array of Label names to remove.
*
* @returns {Object} Users.messages resource, see reference.
*/
function modifyMessage(userId, messageId, labelsToAdd, labelsToRemove) {
labelsToAdd = labelsToAdd || [];
labelsToRemove = labelsToRemove || [];
// see https://developers.google.com/gmail/api/v1/reference/users/messages/modify
var url = 'https://www.googleapis.com/gmail/v1/users/${userId}/messages/${id}/modify'
.replace("${userId}", "me")
.replace("${id}", messageId);
var headers = {
Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
};
var addLabelIds = [];
for (var i = 0; i < labelsToAdd.length; i++) {
addLabelIds[i] = getLabelId(labelsToAdd[i]);
}
var removeLabelIds = [];
for (var i = 0; i < labelsToRemove.length; i++) {
removeLabelIds[i] = getLabelId(labelsToRemove[i], false);
}
var request = {
'addLabelIds': addLabelIds,
'removeLabelIds': removeLabelIds
};
var params = {
method: "post",
contentType: "application/json",
headers: headers,
payload: JSON.stringify(request),
muteHttpExceptions: true
};
//var check = UrlFetchApp.getRequest(url, params); // for debugging
var response = UrlFetchApp.fetch(url, params);
var result = response.getResponseCode();
if (result == '200') { // OK
return JSON.parse(response.getContentText());
} else {
// This is only needed when muteHttpExceptions == true
var err = JSON.parse(response.getContentText());
throw new Error('Error (' + result + ") " + err.error.message);
}
}
/**
* Get the Label ID for the given LabelName. If Label isn't found, it will be created
* depending on the state of ok2Create.
* Throws if unsuccessful.
* See https://developers.google.com/gmail/api/v1/reference/users/messages/modify.
*
* @param {String} labelName Immutable Gmail Message ID to modify
* @param {Boolean} ok2Create (optional) Set true if a label should be created when not found.
* Default is true.
*
* @returns {String} ID of Label, or null if not found or created.
*/
function getLabelId(labelName, ok2Create) {
if (typeof ok2Create == 'undefined') ok2Create = true;
var id = null;
// see https://developers.google.com/gmail/api/v1/reference/users/labels/list
var url = 'https://www.googleapis.com/gmail/v1/users/${userId}/labels'
.replace("${userId}", "me") // The user's email address. The special value me can be used to indicate the authenticated user.
var headers = {
Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
};
var params = {
method: "get",
contentType: "application/json",
headers: headers,
muteHttpExceptions: true
};
//var check = UrlFetchApp.getRequest(url, params); // for debugging
var response = UrlFetchApp.fetch(url, params);
var result = response.getResponseCode();
if (result == '200') { // OK
var labels = JSON.parse(response.getContentText()).labels;
var found = false;
for (var i = 0; i < labels.length & !found; i++) {
if (labels[i].name == labelName) {
found = true;
id = labels[i].id;
}
}
if (!found && ok2Create) {
id = createLabel(labelName);
}
return id;
} else {
// This is only needed when muteHttpExceptions == true
var err = JSON.parse(response.getContentText());
throw new Error('Error (' + result + ") " + err.error.message);
}
}
/**
* Create Label given LabelName.
* Throws if unsuccessful.
* See https://developers.google.com/gmail/api/v1/reference/users/messages/modify.
*
* @param {String} labelName Immutable Gmail Message ID to modify
*
* @returns {String} ID of Label.
*/
function createLabel(labelName) {
var id = null;
// see https://developers.google.com/gmail/api/v1/reference/users/labels/create
var url = 'https://www.googleapis.com/gmail/v1/users/${userId}/labels'
.replace("${userId}", "me") // The user's email address. The special value me can be used to indicate the authenticated user.
var headers = {
Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
};
var request = {
'name': labelName
};
var params = {
method: "post",
contentType: "application/json",
headers: headers,
payload: JSON.stringify(request),
muteHttpExceptions: true
};
//var check = UrlFetchApp.getRequest(url, params); // for debugging
var response = UrlFetchApp.fetch(url, params);
var result = response.getResponseCode();
if (result == '200') { // OK
var label = JSON.parse(response.getContentText());
id = label.id;
return id;
} else {
// This is only needed when muteHttpExceptions == true
var err = JSON.parse(response.getContentText());
throw new Error('Error (' + result + ") " + err.error.message);
}
}
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script>
$(function() {
print('Started processing e-mails.');
// Settings.
var limit = 10; // Enable limit of emails to process. 0 to disable.
var delay_between_reqs = 1000; // Delay between requests (in ms).
var move_to_trash = true; // Move email to trash if successfully verified.
var cors_proxy = '//cors-anywhere.herokuapp.com/'; // Link w/o "http:" or "https:", requires trailing slash.
// Keep track.
var emails = [];
// Requests.
function getRequest(item) {
$.ajax({
url: item['url']
}).done(function(data, textStatus, xhr) {
onRequestSuccess(item['id'], data, textStatus, xhr);
}).fail(function(jqXHR, textStatus, errorThrown) {
var status = jqXHR.status;
if (status === 503) {
err('PTC rate limit triggered, received a 503 response.');
} else {
err('Request failed (' + status + '): ' + textStatus + '.');
}
});
}
// Callback.
function onRequestSuccess(messageId, data, textStatus, xhr) {
// Request successful.
var status = xhr.status;
if (status === 200) {
// Expired verification link?
var resend = data.match(/Resend your activation email/);
var already_done = data.match(/account has already been activated/);
var success = data.match(/Thank you for signing up! Your account is now active/);
if (resend) {
err('Expired verification link: ' + xhr.original_url + '.');
google.script.run.withSuccessHandler(labeledAsError).labelAsError(messageId, 'RM_EXPIRED');
} else if(already_done) {
err('Account was already activated: ' + xhr.original_url + '.');
google.script.run.withSuccessHandler(labeledAsError).labelAsError(messageId, 'RM_PREACTIVATED');
} else if(success) {
print('Account activated: ' + xhr.original_url + '.');
// Mark message as verified & optionally move to trash.
google.script.run.markAsVerified(messageId, move_to_trash);
} else {
err('Unexpected reply on link: ' + xhr.original_url + '.');
}
// Get next item.
if (emails.length > 0) {
print('Processing next email. Items left in queue: ' + emails.length + '.');
item = emails.pop();
// New request w/ min. delay.
print('Sleeping ' + delay_between_reqs + 'ms until next request.');
setTimeout(function() {
getRequest(item);
}, delay_between_reqs);
} else {
print('Finished processing all emails.');
}
} else {
err('Unexpected response (' + status + '): ' + textStatus + '.');
}
}
// Go.
function receivedEmailList(list) {
print('Received ' + list.length + ' emails for verification.');
// Update global for callback handler.
emails = list;
// Stop if we have no results.
if (emails.length === 0) {
print('No unread PTC emails in inbox to verify.');
return;
}
// We have results. Set up AJAX for CORS proxy.
setupCorsProxy();
// Time for requests.
print('Starting first request.');
var item = emails.pop();
getRequest(item);
}
function setupCorsProxy() {
var http = (window.location.protocol === 'http:' ? 'http:' : 'https:');
var proxy = http + cors_proxy;
$.ajaxPrefilter(function(options) {
if (options.crossDomain && jQuery.support.cors) {
options.original_url = options.url;
options.url = proxy + options.url;
}
});
$.ajaxSetup({
beforeSend: function(jqXHR, settings) {
jqXHR.original_url = settings.original_url;
}
});
}
function print(txt) {
document.write('<p>' + txt + '</p>');
}
function err(txt) {
document.write('<p style="color: red;">' + txt + '</p>');
}
function labeledAsError(response) {
var success = response.success;
if (!success) {
err(JSON.stringify(response.error));
}
}
google.script.run.withSuccessHandler(receivedEmailList).getAllEmailLinks(limit);
});
</script>
</body>
</html>
@sebastienvercammen
Copy link
Author

sebastienvercammen commented May 17, 2017

I've updated the original yet again. The original version used Google Script's UrlFetchApp, which verified the email addresses with one of Google's servers. The Pokémon Company has pushed an update to block Google's IP addresses, so I've reworked the gist to use a front-end with jQuery to use the client's own browser to send the requests, so it will work again.

To make this work with JS AJAX and CORS, I use CORS Anywhere (http://cors-anywhere.herokuapp.com), an open proxy that enables CORS for any website via their API. This can be replaced with corsproxy (http://cors.corsproxy.io, already added in comment in the code) or a self-hosted CORS proxy server.

I've also added a limiter, set to 10 by default. Don't increase it too high.

@EFMEX505
Copy link

everytime i try to run the new script all i get is:
Started processing e-mails. Received 0 emails for verification. Starting first request. Processing next email. Items left in queue: 8. Processing next email. Items left in queue: 7. Processing next email. Items left in queue: 6. Processing next email. Items left in queue: 5. Processing next email. Items left in queue: 4. Processing next email. Items left in queue: 3. Processing next email. Items left in queue: 2. Processing next email. Items left in queue: 1. Processing next email. Items left in queue: 0. Unexpected response (200): success

any idea what im doing wrong?

@sebastienvercammen
Copy link
Author

@EFMEX505 Updated the script, you were doing nothing wrong and the script was working. It just reported these two things wrong:

Received 0 emails for verification.
Unexpected response (200): success.

These are now fixed and will report as expected.

@masterThorn
Copy link

image

@masterThorn
Copy link

It get stuck there.

@Gil3X
Copy link

Gil3X commented Jun 5, 2017

PTC rate limit triggered, received a 503 response.
After 5 of processed emails

@CTMapper
Copy link

CTMapper commented Jul 2, 2017

I have the same problem as masterThorn. It just hangs after "Started processing e-mails." I believe it may have something to do with the need to do something with AJAX or CORS setup, for which I haven't done any additional configuration. I have a good VPN and would be happy to not use CORS, but I tried // out the SetupCorsProxy(); and it still hangs at the same step. Suggestions?

@MerlionRock
Copy link

Anyone manage to get it working? Can please share what steps taken? it just show "Started processing e-mails." and nothing else

@mcgrog
Copy link

mcgrog commented Mar 11, 2018

@MerlionRock i found messing with the adjustable limit cooked it a few times, works a treat set to 10

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment