Skip to content

Instantly share code, notes, and snippets.

@powmod
Forked from eudamniac/remarkableNotionSync.gs
Last active October 1, 2025 20:50
Show Gist options
  • Select an option

  • Save powmod/f248ab6147d0664a96da5e448e0622eb to your computer and use it in GitHub Desktop.

Select an option

Save powmod/f248ab6147d0664a96da5e448e0622eb to your computer and use it in GitHub Desktop.
reMarkable to Notion Sync
const GMAIL_LABEL_NAME = 'NotionToSync';
const SYNCED_LABEL = 'SyncedToNotion';
const GDRIVE_FOLDER_ID = ''; // optional; leave empty to upload directly to Notion
const NOTION_DATABASE_ID = '';
const NOTION_SECRET = '';
const NOTION_VERSION = '2022-06-28';
// Direct-upload cap per Notion guide (multi-part required >20MB). :contentReference[oaicite:1]{index=1}
const MAX_DIRECT_UPLOAD_BYTES = 20 * 1024 * 1024;
const gmailToNotion = () => {
const label = GmailApp.getUserLabelByName(GMAIL_LABEL_NAME);
const successLabel = GmailApp.getUserLabelByName(SYNCED_LABEL);
if (!label) return;
label.getThreads(0, 20).forEach(thread => {
const [message] = thread.getMessages().reverse();
try {
postToNotion(message);
thread.removeLabel(label);
thread.addLabel(successLabel);
} catch (e) {
Logger.log(`Error: ${e}`);
}
});
};
function getRichTextChunks(s) {
let t = s || '';
const out = [];
while (t.length > 0) {
if (t.length <= 2000) {
out.push(richText(t));
t = '';
} else {
const cut = t.substring(0, 2000);
const br = cut.lastIndexOf('\n');
const chunk = t.substring(0, br > 0 ? br : 2000);
out.push(richText(chunk));
t = t.substring(chunk.length + (br > 0 ? 1 : 0));
}
}
return out;
}
const richText = content => ({ type: 'text', text: { content } });
function getFileBlocksForAttachments(message) {
const atts = message.getAttachments();
const blocks = [];
if (!atts || !atts.length) return blocks;
for (const a of atts) {
const name = a.getName();
const type = a.getContentType() || '';
try {
if (isPDF(name, type)) {
blocks.push(...handlePDFAttachment(a));
} else if (isPNG(name, type) || isSVG(name, type)) {
blocks.push(...handleImageAttachment(a)); // displays PNG or SVG
} else if (isGenericImage(type)) {
blocks.push(...handleImageAttachment(a)); // jpeg, webp, etc.
} else {
blocks.push(...handleGenericAttachment(a));
}
} catch (e) {
blocks.push(errorBlock(name, e.toString()));
}
}
return blocks;
}
function isPDF(name, ctype) {
return name.toLowerCase().endsWith('.pdf') || ctype === 'application/pdf';
}
function isPNG(name, ctype) {
const n = name.toLowerCase();
return n.endsWith('.png') || ctype === 'image/png';
}
function isSVG(name, ctype) {
const n = name.toLowerCase();
return n.endsWith('.svg') || ctype === 'image/svg+xml';
}
function isGenericImage(ctype) {
return typeof ctype === 'string' && ctype.startsWith('image/');
}
function handlePDFAttachment(att) {
const name = att.getName();
const size = att.getSize();
const header = heading(`πŸ“„ ${name}`);
// Attempt Notion native upload first
try {
const block = uploadAsBlock(att, 'pdf', size);
return [header, block, infoBlock(size)];
} catch (e) {
// Fallback to Drive link if configured
if (GDRIVE_FOLDER_ID) {
const file = uploadToGoogleDrive(att);
return [
header,
paragraphLink(`View PDF: ${name}`, file.getUrl()),
infoBlock(size),
warnBlock('Uploaded to Drive due to Notion upload failure or size limit.')
];
}
return [header, warnBlock(`PDF "${name}" not uploaded: ${e.toString()}`)];
}
}
function handleImageAttachment(att) {
const name = att.getName();
const size = att.getSize();
const header = heading(`πŸ–ΌοΈ ${name}`);
try {
const block = uploadAsBlock(att, 'image', size); // supports PNG, SVG, others
return [header, block, infoBlock(size)];
} catch (e) {
if (GDRIVE_FOLDER_ID) {
const file = uploadToGoogleDrive(att);
return [
header,
paragraphLink(`View image: ${name}`, file.getUrl()),
infoBlock(size),
warnBlock('Uploaded to Drive due to Notion upload failure or size limit.')
];
}
return [header, warnBlock(`Image "${name}" not uploaded: ${e.toString()}`)];
}
}
function handleGenericAttachment(att) {
const name = att.getName();
const size = att.getSize();
// Try to attach as a generic file block via Notion upload
try {
const fu = uploadToNotionSmall(att);
return [{
object: 'block',
type: 'file',
file: { type: 'file_upload', file_upload: { id: fu.id } }
}, infoBlock(size)];
} catch (e) {
if (GDRIVE_FOLDER_ID) {
const file = uploadToGoogleDrive(att);
return [paragraphLink(`πŸ“Ž ${name}`, file.getUrl()), infoBlock(size)];
}
return [paragraph(`πŸ“Ž ${name} (${(size / 1024).toFixed(1)} KB)`), warnBlock(`File not uploaded: ${e.toString()}`)];
}
}
// Core upload helper -> returns a block with type 'image' or 'pdf'
function uploadAsBlock(attachment, kind /* 'image' | 'pdf' */, size) {
if (size > MAX_DIRECT_UPLOAD_BYTES) throw new Error('exceeds 20MB direct-upload limit');
const fu = uploadToNotionSmall(attachment);
if (kind === 'image') {
return {
object: 'block',
type: 'image',
image: { type: 'file_upload', file_upload: { id: fu.id } }
};
} else if (kind === 'pdf') {
return {
object: 'block',
type: 'pdf',
pdf: { type: 'file_upload', file_upload: { id: fu.id } }
};
}
throw new Error('unsupported kind');
}
// Notion direct upload (single-part, ≀20MB). :contentReference[oaicite:2]{index=2}
function uploadToNotionSmall(attachment) {
const blobSrc = attachment.copyBlob();
const blob = Utilities.newBlob(blobSrc.getBytes(), blobSrc.getContentType(), attachment.getName());
const createRes = UrlFetchApp.fetch('https://api.notion.com/v1/file_uploads', {
method: 'post',
contentType: 'application/json',
headers: { Authorization: `Bearer ${NOTION_SECRET}`, 'Notion-Version': NOTION_VERSION },
payload: JSON.stringify({ filename: attachment.getName(), content_type: blob.getContentType() })
});
if (createRes.getResponseCode() !== 200) throw new Error(`create failed: ${createRes.getContentText()}`);
const { id } = JSON.parse(createRes.getContentText());
// Important: do not set contentType; Apps Script sets multipart/form-data with boundary when payload has a Blob.
const sendRes = UrlFetchApp.fetch(`https://api.notion.com/v1/file_uploads/${id}/send`, {
method: 'post',
headers: { Authorization: `Bearer ${NOTION_SECRET}`, 'Notion-Version': NOTION_VERSION },
payload: { file: blob },
muteHttpExceptions: true
});
if (sendRes.getResponseCode() !== 200) throw new Error(`send failed: ${sendRes.getContentText()}`);
return { id };
}
function uploadToGoogleDrive(att) {
const folder = DriveApp.getFolderById(GDRIVE_FOLDER_ID);
const ts = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyyMMdd_HHmmss');
const fname = `${att.getName()}_${ts}`;
const file = folder.createFile(att.copyBlob()).setName(fname);
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
return file;
}
// UI helpers
const heading = text => ({ object: 'block', type: 'heading_3', heading_3: { rich_text: [{ type: 'text', text: { content: text } }] } });
const paragraph = text => ({ object: 'block', type: 'paragraph', paragraph: { rich_text: [{ type: 'text', text: { content: text } }] } });
const paragraphLink = (text, url) => ({ object: 'block', type: 'paragraph', paragraph: { rich_text: [{ type: 'text', text: { content: text, link: { url } } }] } });
const infoBlock = size => ({ object: 'block', type: 'callout', callout: { rich_text: [{ type: 'text', text: { content: `File size: ${(size / 1024).toFixed(1)} KB` } }], icon: { emoji: 'ℹ️' } } });
const warnBlock = msg => ({ object: 'block', type: 'callout', callout: { rich_text: [{ type: 'text', text: { content: msg } }], icon: { emoji: '⚠️' } } });
const errorBlock = (name, msg) => ({ object: 'block', type: 'callout', callout: { rich_text: [{ type: 'text', text: { content: `Failed to process attachment: ${name}. Error: ${msg}` } }], icon: { emoji: '❌' } } });
function postToNotion(message) {
const body = message.getPlainBody() || '';
const subject = message.getSubject() || 'No Subject';
const date = Utilities.formatDate(message.getDate(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm:ss');
const children = [];
children.push({ object: 'block', type: 'callout', callout: { rich_text: [{ type: 'text', text: { content: `πŸ“§ Synced from email on ${date}` } }], icon: { emoji: 'πŸ“…' } } });
children.push(...getFileBlocksForAttachments(message));
if (body.trim()) {
children.push(heading('πŸ“ Notes'));
children.push({ object: 'block', type: 'paragraph', paragraph: { rich_text: getRichTextChunks(body) } });
}
const url = 'https://api.notion.com/v1/pages';
const payload = {
parent: { type: 'database_id', database_id: NOTION_DATABASE_ID },
icon: { type: 'emoji', emoji: 'πŸ“„' },
children: children,
properties: { Name: { title: [{ text: { content: subject } }] } }
};
const res = UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
headers: { Authorization: `Bearer ${NOTION_SECRET}`, 'Notion-Version': NOTION_VERSION },
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
if (res.getResponseCode() !== 200) throw new Error(`Notion API error: ${res.getResponseCode()} ${res.getContentText()}`);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment