/** * GED Hub Gmail gateway. Install in an Apps Script project owned by * gedhub.info@gmail.com. This script sends email only; it does not read the inbox. * * 1. Run createGatewaySecret() once and approve the requested Google permissions. * 2. Open Project Settings > Script properties and copy GEDHUB_SECRET privately. * 3. Deploy as Web app: Execute as Me, Who has access Anyone. * 4. Paste the /exec URL and GEDHUB_SECRET into GED Hub Admin > Email Notifications. * Never put the secret in chat, source code, URLs, or client-side JavaScript. */ var GEDHUB_SENDER = 'gedhub.info@gmail.com'; function createGatewaySecret() { if (Session.getEffectiveUser().getEmail().toLowerCase() !== GEDHUB_SENDER) { throw new Error('Open this project using ' + GEDHUB_SENDER); } var properties = PropertiesService.getScriptProperties(); if (!properties.getProperty('GEDHUB_SECRET')) { properties.setProperty('GEDHUB_SECRET', (Utilities.getUuid() + Utilities.getUuid()).replace(/-/g, '')); } MailApp.getRemainingDailyQuota(); console.log('Ready. Copy GEDHUB_SECRET from Project Settings > Script properties into GED Hub Admin.'); } function gatewayReply(value) { return ContentService.createTextOutput(JSON.stringify(value)).setMimeType(ContentService.MimeType.JSON); } function gatewayHex(bytes) { return bytes.map(function (b) { return ('0' + ((b + 256) % 256).toString(16)).slice(-2); }).join(''); } function gatewayEqual(a, b) { var different = a.length ^ b.length; for (var i = 0; i < Math.max(a.length, b.length); i++) different |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0); return different === 0; } function doGet() { return gatewayReply({service: 'GED Hub email gateway',version:2,capabilities:['pdf-attachments']}); } function doPost(e) { var lock = LockService.getScriptLock(); var locked = false; try { var properties = PropertiesService.getScriptProperties(); var secret = properties.getProperty('GEDHUB_SECRET'); if (!secret || !e || !e.postData || e.postData.contents.length > 2000000) return gatewayReply({ok: false, code: 'not_configured'}); var envelope = JSON.parse(e.postData.contents); if (typeof envelope.payload !== 'string' || typeof envelope.signature !== 'string') return gatewayReply({ok: false, code: 'invalid_request'}); var expected = gatewayHex(Utilities.computeHmacSha256Signature(envelope.payload, secret, Utilities.Charset.UTF_8)); if (!gatewayEqual(expected, envelope.signature)) return gatewayReply({ok: false, code: 'invalid_signature'}); var message = JSON.parse(envelope.payload); if (typeof message.id !== 'string' || message.id.length > 200 || typeof message.to !== 'string' || !/^[^\s@,;\r\n]+@[^\s@,;\r\n]+\.[^\s@,;\r\n]+$/.test(message.to) || typeof message.subject !== 'string' || message.subject.length > 200 || /[\r\n]/.test(message.subject) || typeof message.body !== 'string' || message.body.length > 16000 || !Number.isFinite(message.timestamp) || Math.abs(Date.now() - message.timestamp) > 300000) return gatewayReply({ok: false, code: 'invalid_request'}); if (Session.getEffectiveUser().getEmail().toLowerCase() !== GEDHUB_SENDER) return gatewayReply({ok: false, code: 'wrong_sender'}); var attachments = []; if (message.attachments !== undefined) { if (!Array.isArray(message.attachments) || message.attachments.length > 1) return gatewayReply({ok:false,code:'invalid_attachment'}); for (var a = 0; a < message.attachments.length; a++) { var item = message.attachments[a]; if (!item || item.contentType !== 'application/pdf' || typeof item.filename !== 'string' || !/^[A-Za-z0-9_-]+\.pdf$/.test(item.filename) || typeof item.contentBase64 !== 'string' || item.contentBase64.length > 1500000 || !/^[A-Za-z0-9+/]+={0,2}$/.test(item.contentBase64)) return gatewayReply({ok:false,code:'invalid_attachment'}); var pdfBytes = Utilities.base64Decode(item.contentBase64); if (pdfBytes.length < 5 || String.fromCharCode.apply(null,pdfBytes.slice(0,5)) !== '%PDF-') return gatewayReply({ok:false,code:'invalid_attachment'}); attachments.push(Utilities.newBlob(pdfBytes,'application/pdf',item.filename)); } } lock.waitLock(15000); locked = true; var key = 'mail_' + gatewayHex(Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, message.id, Utilities.Charset.UTF_8)); var stored = properties.getProperty(key); if (stored) { var previous = JSON.parse(stored); if (previous.status === 'sent') return gatewayReply({ok: true, id: message.id, duplicate: true,attachmentCount:previous.attachmentCount||0}); return gatewayReply({ok: false, id: message.id, code: 'delivery_uncertain'}); } if (MailApp.getRemainingDailyQuota() < 1) return gatewayReply({ok: false, code: 'quota_exceeded'}); // Mark before sending. If execution stops during delivery, do not send again // automatically: staff should check Sent mail to avoid a duplicate message. properties.setProperty(key, JSON.stringify({status: 'sending',at: Date.now()})); try { var mailOptions={to:message.to,subject:message.subject,body:message.body,name:'GED Hub',replyTo:'gedhub.contact@gmail.com'}; if(attachments.length)mailOptions.attachments=attachments; MailApp.sendEmail(mailOptions); properties.setProperty(key, JSON.stringify({status: 'sent',at: Date.now(),attachmentCount:attachments.length})); } catch (error) { return gatewayReply({ok: false, id: message.id, code: 'delivery_uncertain'}); } // Retain sent keys for 60 days. Uncertain deliveries are kept for review. var all = properties.getProperties(); Object.keys(all).forEach(function (name) { if (name.indexOf('mail_') !== 0) return; try {var item = JSON.parse(all[name]); if (item.status === 'sent' && item.at < Date.now() - 60 * 86400000) properties.deleteProperty(name);} catch (ignored) {} }); return gatewayReply({ok: true, id: message.id,attachmentCount:attachments.length}); } catch (error) { return gatewayReply({ok: false, code: 'gateway_error'}); } finally { if (locked) lock.releaseLock(); } }