Skip to content

Instantly share code, notes, and snippets.

@maluramichael
Created November 6, 2025 09:07
Show Gist options
  • Select an option

  • Save maluramichael/20dee51127c3fe4eeaed1f438bb366a4 to your computer and use it in GitHub Desktop.

Select an option

Save maluramichael/20dee51127c3fe4eeaed1f438bb366a4 to your computer and use it in GitHub Desktop.
ING Postbox PDF Downloader
// ==UserScript==
// @name ING Postbox PDF Downloader
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Safely download all PDFs from ING Postbox Archive with rate limiting
// @author Michael Malura <[email protected]>
// @match https://banking.ing.de/app/postbox/postbox_archiv*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
delayBetweenDownloads: 2000,
buttonColor: '#FF6200',
checkmarkColor: '#00A651',
buttonText: 'Download All PDFs'
};
let isDownloading = false;
let shouldAbort = false;
let downloadQueue = [];
let processedCount = 0;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function createCheckmark() {
const checkmark = document.createElement('span');
checkmark.innerHTML = '✓';
checkmark.style.cssText = `
display: inline-block;
margin-left: 8px;
color: ${CONFIG.checkmarkColor};
font-weight: bold;
font-size: 18px;
animation: fadeIn 0.3s ease-in;
`;
return checkmark;
}
function markRowAsProcessed(row) {
row.style.backgroundColor = '#f0f8f0';
row.style.opacity = '0.7';
const docTypeCell = row.querySelector('.postbox-grid-left span:last-child');
if (docTypeCell && !docTypeCell.querySelector('span[data-downloaded]')) {
const checkmark = createCheckmark();
checkmark.setAttribute('data-downloaded', 'true');
docTypeCell.appendChild(checkmark);
}
}
function extractDownloadLinks() {
const links = [];
const rows = document.querySelectorAll('.ibbr-table-row[role="row"]');
rows.forEach(row => {
if (row.querySelector('span[data-downloaded]')) {
return;
}
const downloadLink = row.querySelector('a.button[role="button"][href*="postbox_archiv"]');
if (downloadLink && downloadLink.textContent.trim() === 'Download') {
const docType = row.querySelector('.postbox-grid-left span:last-child')?.textContent.trim() || 'Unknown';
const description = row.querySelector('.postbox-grid-description')?.textContent.trim() || '';
const date = row.querySelector('.postbox-grid-right')?.textContent.trim() || '';
links.push({
url: downloadLink.href,
row: row,
docType: docType,
description: description.replace(/\s+/g, ' '),
date: date
});
}
});
return links;
}
async function downloadPDF(linkData, index, total) {
try {
console.log(`[${index + 1}/${total}] Downloading: ${linkData.docType} - ${linkData.date}`);
updateStatus(`Downloading ${index + 1}/${total}: ${linkData.docType}...`);
const tempLink = document.createElement('a');
tempLink.href = linkData.url;
tempLink.style.display = 'none';
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
markRowAsProcessed(linkData.row);
processedCount++;
console.log(`Successfully queued download ${index + 1}/${total}`);
} catch (error) {
console.error(`Error downloading ${linkData.docType}:`, error);
updateStatus(`Error on ${index + 1}/${total}: ${error.message}`, true);
}
}
function abortDownload() {
shouldAbort = true;
console.log('Download abort requested by user');
updateStatus(`Aborting after current download...`, false);
}
async function processDownloadQueue() {
if (isDownloading) {
alert('Download already in progress!');
return;
}
downloadQueue = extractDownloadLinks();
if (downloadQueue.length === 0) {
alert('No PDFs found to download, or all have already been downloaded.');
return;
}
if (!confirm(`Found ${downloadQueue.length} PDF(s) to download.\n\nThis will download them with a ${CONFIG.delayBetweenDownloads/1000}s delay between each.\n\nContinue?`)) {
return;
}
isDownloading = true;
shouldAbort = false;
processedCount = 0;
const startButton = document.getElementById('ing-pdf-downloader-btn');
const abortButton = document.getElementById('ing-pdf-abort-btn');
if (startButton) {
startButton.style.display = 'none';
}
if (abortButton) {
abortButton.style.display = 'block';
}
console.log(`Starting download of ${downloadQueue.length} PDFs...`);
updateStatus(`Starting download of ${downloadQueue.length} PDFs...`);
for (let i = 0; i < downloadQueue.length; i++) {
if (shouldAbort) {
console.log(`Download aborted by user after ${processedCount} files`);
updateStatus(`Aborted! ${processedCount}/${downloadQueue.length} PDFs downloaded.`, false, 5000);
break;
}
await downloadPDF(downloadQueue[i], i, downloadQueue.length);
if (i < downloadQueue.length - 1 && !shouldAbort) {
const remainingDelay = CONFIG.delayBetweenDownloads;
updateStatus(`Waiting ${remainingDelay/1000}s before next download...`);
await sleep(remainingDelay);
}
}
isDownloading = false;
shouldAbort = false;
if (processedCount === downloadQueue.length) {
console.log(`All downloads completed! (${processedCount}/${downloadQueue.length} successful)`);
updateStatus(`Completed! ${processedCount}/${downloadQueue.length} PDFs downloaded.`, false, 5000);
}
if (startButton) {
startButton.style.display = 'block';
}
if (abortButton) {
abortButton.style.display = 'none';
}
}
function updateStatus(message, isError = false, autoClear = 0) {
const statusDiv = document.getElementById('ing-pdf-status');
if (statusDiv) {
statusDiv.textContent = message;
statusDiv.style.color = isError ? '#d32f2f' : '#333';
if (autoClear > 0) {
setTimeout(() => {
statusDiv.textContent = '';
}, autoClear);
}
}
}
function createDownloadButton() {
if (document.getElementById('ing-pdf-downloader-btn')) {
return;
}
const container = document.createElement('div');
container.id = 'ing-pdf-downloader-container';
container.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
font-family: Arial, sans-serif;
min-width: 250px;
`;
const button = document.createElement('button');
button.id = 'ing-pdf-downloader-btn';
button.textContent = CONFIG.buttonText;
button.style.cssText = `
background: ${CONFIG.buttonColor};
color: white;
border: none;
padding: 12px 20px;
font-size: 14px;
font-weight: bold;
border-radius: 4px;
cursor: pointer;
width: 100%;
transition: background 0.3s;
display: block;
`;
button.onmouseover = () => button.style.background = '#E55A00';
button.onmouseout = () => button.style.background = CONFIG.buttonColor;
button.onclick = processDownloadQueue;
const abortButton = document.createElement('button');
abortButton.id = 'ing-pdf-abort-btn';
abortButton.textContent = '🛑 Abort Download';
abortButton.style.cssText = `
background: #d32f2f;
color: white;
border: none;
padding: 12px 20px;
font-size: 14px;
font-weight: bold;
border-radius: 4px;
cursor: pointer;
width: 100%;
transition: background 0.3s;
display: none;
`;
abortButton.onmouseover = () => abortButton.style.background = '#b71c1c';
abortButton.onmouseout = () => abortButton.style.background = '#d32f2f';
abortButton.onclick = abortDownload;
const statusDiv = document.createElement('div');
statusDiv.id = 'ing-pdf-status';
statusDiv.style.cssText = `
margin-top: 10px;
font-size: 12px;
color: #666;
min-height: 20px;
word-wrap: break-word;
`;
const infoDiv = document.createElement('div');
infoDiv.style.cssText = `
margin-top: 8px;
font-size: 11px;
color: #999;
border-top: 1px solid #eee;
padding-top: 8px;
`;
infoDiv.innerHTML = `
Delay: ${CONFIG.delayBetweenDownloads/1000}s between downloads<br>
= Downloaded
`;
container.appendChild(button);
container.appendChild(abortButton);
container.appendChild(statusDiv);
container.appendChild(infoDiv);
document.body.appendChild(container);
const style = document.createElement('style');
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: scale(0); }
to { opacity: 1; transform: scale(1); }
}
`;
document.head.appendChild(style);
console.log('ING PDF Downloader: Button injected successfully');
}
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
return;
}
if (!window.location.href.includes('postbox_archiv')) {
return;
}
setTimeout(() => {
const table = document.querySelector('.ibbr-table-wrapper-postbox');
if (table) {
createDownloadButton();
console.log('ING PDF Downloader: Ready');
} else {
console.log('ING PDF Downloader: Table not found, retrying...');
setTimeout(init, 1000);
}
}, 500);
}
init();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment