// Name: Messages 2FA codes // Description: Search for 2FA codes in your Messages, within the last 30 minutes // Ackowledgements: // - https://github.com/squatto/alfred-imessage-2fa/ // - https://github.com/raycast/extensions/tree/main/extensions/imessage-2fa import "@johnlindquist/kit" import Database from 'better-sqlite3'; let preferences = { lookBackMinutes: 30, ignoreRead: false, } export type TMessage = { guid: string; message_date: string; // 2024-11-26 06:11:18 sender: string; // e.g. amazon.de or +49123456789 service: string; // e.g. SMS text: string; } const db = new Database(home("Library/Messages/chat.db")); let output = await arg({ placeholder: "Select a message or start typing to search", choices: async (input) => { let stmt = db.prepare(dbQuery(input)); let messages = stmt.all() as TMessage[]; return messages.map((m) => ({ name: m.text, tag: extractCode(m.text) ?? "no code", description: `${m.message_date} • ${m.sender} • ${m.service}`, value: m.text, preview: `<div class="p-2 text-sm">${m.text}</div>`, })); }, actions: [ { name: "Copy Whole Message", flag: "copyWholeMessage", visible: true, shortcut: `${cmd}+c`, }, ] }) if (flag.copyWholeMessage) { clipboard.writeText(output) notify("Whole message copied to clipboard") } else { let code = extractCode(output) if (code) { clipboard.writeText(code) notify(`Code: ${code} copied to clipboard`) } else { clipboard.writeText(output) notify("No code found. Copied whole message to clipboard instead") } } // Helpers // === function dbQuery(qs: string = "") { let baseQuery = /* sql */` select message.guid, message.rowid, ifnull(handle.uncanonicalized_id, chat.chat_identifier) AS sender, message.service, datetime(message.date / 1000000000 + 978307200, 'unixepoch', 'localtime') AS message_date, message.text from message left join chat_message_join on chat_message_join.message_id = message.ROWID left join chat on chat.ROWID = chat_message_join.chat_id left join handle on message.handle_id = handle.ROWID where message.is_from_me = 0 and message.text is not null and length(message.text) > 0 and datetime(message.date / 1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') >= datetime('now', '-${preferences.lookBackMinutes} minutes', 'localtime') `; if (preferences.ignoreRead) baseQuery += " and message.is_read = 0"; if (!qs) { // search for code baseQuery = /* sql */`${baseQuery} and ( -- Matches 3 alphanumeric (e.g., 'ABC') message.text glob '*[0-9A-Z][0-9A-Z][0-9A-Z]*' -- Matches 4 alphanumeric (e.g., 'ABCD') or message.text glob '*[0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z]*' -- Matches 5 alphanumeric (e.g., 'ABCDE') or message.text glob '*[0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z]*' -- Matches 6 alphanumeric (e.g., 'ABCDEF') or message.text glob '*[0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z]*' -- Matches format '123-456' or message.text glob '*[0-9][0-9][0-9]-[0-9][0-9][0-9]*' -- Matches 7 alphanumeric (e.g., 'ABCDEFG') or message.text glob '*[0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z]*' -- Matches 8 alphanumeric (e.g., 'ABCDEFGH') or message.text glob '*[0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z]*' )`; } else { // Search for text baseQuery = /* sql */`${baseQuery} and message.text like '%${qs}%'`; } return `${baseQuery} \norder by message.date desc limit 100`.trim(); } export function extractCode(original: string) { // remove URLs const urlRegex = new RegExp( "\\b((https?|ftp|file):\\/\\/|www\\.)[-A-Z0-9+&@#\\/%?=~_|$!:,.;]*[A-Z0-9+&@#\\/%=~_|$]", "ig" ); let message = original.replaceAll(urlRegex, ""); if (message.trim() === "") return ""; let m; let code; // Look for specific patterns first if ((m = /^(\d{4,8})(\sis your.*code)/.exec(message)) !== null) { // 4-8 digits followed by "is your [...] code" // examples: // "2773 is your Microsoft account verification code" code = m[1]; } else if ( (m = /(code\s*:|is\s*:|码|use code|autoriza(?:ca|çã)o\s*:|c(?:o|ó)digo\s*:)\s*(\w{4,8})($|\s|\\R|\t|\b|\.|,)/i.exec( message )) !== null ) { // "code:" OR "is:" OR "use code", optional whitespace, then 4-8 consecutive alphanumeric characters // examples: // "Your Airbnb verification code is: 1234." // "Your verification code is: 1234, use it to log in" // "Here is your authorization code:9384" // "【抖音】验证码9316,用于手机验证" // "Your healow verification code is : 7579." // "TRUSTED LOCATION PASSCODE: mifsuc" // "Código de Autorização: 12345678" code = m[2]; } else { // more generic, brute force patterns // remove phone numbers // we couldn't do this before, because some auth codes resemble text shortcodes, which would be filtered by this regex const phoneRegex = new RegExp( // https://stackoverflow.com/a/123666 /(?:(?:\+?1\s*(?:[.-]\s*)?)?(?:\(\s*([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9])\s*\)|([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9]))\s*(?:[.-]\s*)?)?([2-9]1[02-9]|[2-9][02-9]1|[2-9][02-9]{2})\s*(?:[.-]\s*)?([0-9]{4})(?:\s*(?:#|x\.?|ext\.?|extension)\s*(\d+))?/, "ig" ); const originalMessage = message; message = message.replaceAll(phoneRegex, ""); if ((m = /(^|\s|\\R|\t|\b|G-|:)(\d{5,8})($|\s|\\R|\t|\b|\.|,)/.exec(message)) !== null) { // 5-8 consecutive digits // examples: // "您的验证码是 199035,10分钟内有效,请勿泄露" // "登录验证码:627823,您正在尝试【登录】,10分钟内有效" // "【赛验】验证码 54538" // "Enter this code to log in:59678." // "G-315643 is your Google verification code" // "Enter the code 765432, and then click the button to log in." // "Your code is 45678!" // "Your code is:98765!" code = m[2]; } else if ((m = /\b(?=[A-Z]*[0-9])(?=[0-9]*[A-Z])[0-9A-Z]{3,8}\b/.exec(message)) !== null) { // 3-8 character uppercase alphanumeric string, containing at least one letter and one number // examples: // "5WGU8G" // "Your code is: 5WGU8G" // "CWGUG8" // "CWGUG8 is your code" // "7645W453" code = m[0]; } else if ((m = /(^|code:|is:|\b)\s*(\d{3})-(\d{3})($|\s|\\R|\t|\b|\.|,)/i.exec(message)) !== null) { // line beginning OR "code:" OR "is:" OR word boundary, optional whitespace, 3 consecutive digits, a hyphen, then 3 consecutive digits // but NOT a phone number (###-###-####) // examples: // "123-456" // "Your Stripe verification code is: 719-839." // and make sure it isn't a phone number // doesn't match: <first digits>-<second digits>-<4 consecutive digits> const first = m[2]; const second = m[3]; code = `${first}${second}`; } else if ((m = /(code|is):?\s*(\d{3,8})($|\s|\\R|\t|\b|\.|,)/i.exec(originalMessage)) !== null) { // "code" OR "is" followed by an optional ":" + optional whitespace, then 3-8 consecutive digits // examples: // "Please enter code 548 on Zocdoc." code = m[2]; } else { // console.log("no code found in message"); } } return code; }