├── README.md └── Miles-Gmail-unsubscribe.gs /README.md: -------------------------------------------------------------------------------- 1 | # Miles-Gmail-Unsubscribe 2 | 3 | This Google Apps Script automates the process of unsubscribing from unwanted emails in Gmail. Users can label the emails they want to unsubscribe from with the "unsubscribe" label, and the script will search for an unsubscribe link in the email body or a "List-Unsubscribe" header in the raw email content. Additionally, the script checks the content of the unsubscribe landing page to verify if the unsubscribe was successful. If the process appears unsuccessful, the script will mark the email as spam. 4 | 5 | ## Features 6 | 7 | - Automatically processes emails labeled with "unsubscribe" 8 | - Extracts unsubscribe links from email content or "List-Unsubscribe" header 9 | - Follows unsubscribe links and checks for a confirmation message on the resulting webpage 10 | - Marks emails as read and removes the "unsubscribe" label upon successful unsubscribe 11 | - Moves emails to spam folder if the unsubscribe process is unsuccessful 12 | 13 | ## Usage 14 | 15 | 1. Open Google Drive and create a new Google Apps Script file. 16 | 2. Name the file however you'd like. 17 | 3. Copy and paste the entire "Miles-Gmail-unsubscribe.gs" script into the new script file. 18 | 4. Save the script and grant it the necessary permissions. 19 | 5. Label the emails you want to unsubscribe from with the "unsubscribe" label. 20 | 6. Run the `unsubscribeFromEmails` function in the script editor. 21 | 7. Check your email to see if the "unsubscribe" label has been removed from successfully unsubscribed emails, or if the email has been moved to the spam folder for unsuccessful attempts. 22 | 8. You can set up a trigger on the left hand side to run this script hourly. This will allow you to label emails as unsubscribe and not worry about them anymore. 23 | 24 | ## Customization 25 | 26 | You can customize the success phrases in the `checkUnsubscribeSuccess()` function by modifying the `successPhrases` array. Add or remove phrases as needed to improve the accuracy of the unsubscribe success verification. 27 | 28 | ```javascript 29 | var successPhrases = ["you have been unsubscribed", "unsubscription confirmed", "successfully unsubscribed", "your email has been removed"]; 30 | -------------------------------------------------------------------------------- /Miles-Gmail-unsubscribe.gs: -------------------------------------------------------------------------------- 1 | function unsubscribeFromEmails() { 2 | // Get all threads with the "unsubscribe" label 3 | var threads = GmailApp.search('label:unsubscribe'); 4 | 5 | // Loop through each thread 6 | for (var i = 0; i < threads.length; i++) { 7 | // Get all messages in the thread 8 | var messages = threads[i].getMessages(); 9 | 10 | // Loop through each message 11 | for (var j = 0; j < messages.length; j++) { 12 | // Get the message body 13 | var body = messages[j].getBody(); 14 | 15 | // Try to find an unsubscribe link in the message body 16 | var unsubscribeLink = getEmailUnsubscribeLink(body); 17 | 18 | // If an unsubscribe link is found, try to unsubscribe from it 19 | if (unsubscribeLink) { 20 | Logger.log('Unsubscribe link: ' + unsubscribeLink); 21 | var success = followUnsubscribeLink(unsubscribeLink, messages[j]); 22 | if (success) { 23 | threads[i].removeLabel(GmailApp.getUserLabelByName('unsubscribe')); 24 | messages[j].markRead(); 25 | } else { 26 | Logger.log("followUnsubscribeLink returned false. Moving to spam " + messages[j].getFrom()); 27 | threads[i].moveToSpam(); 28 | } 29 | break; 30 | } 31 | // If no unsubscribe link is found, try to find a "List-Unsubscribe" header in the raw email content 32 | // check to see if else 33 | else { 34 | var rawContent = messages[j].getRawContent(); 35 | 36 | if(rawContent){ 37 | Logger.log("Rawcontent is null, Moving to spam " + messages[j].getFrom()); 38 | threads[i].moveToSpam(); 39 | threads[i].removeLabel(GmailApp.getUserLabelByName('unsubscribe')); 40 | messages[j].markRead(); 41 | break; 42 | } 43 | var url = RawListUnsubscribe(rawContent); 44 | 45 | // If a "List-Unsubscribe" header is found, try to unsubscribe using the URL specified in the header 46 | if (url) { 47 | Logger.log(url); 48 | var status = UrlFetchApp.fetch(url).getResponseCode(); 49 | Logger.log("Unsubscribe " + status + " " + url); 50 | threads[i].removeLabel(GmailApp.getUserLabelByName('unsubscribe')); 51 | messages[j].markRead(); 52 | break; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * Attempts to extract the URL specified in the "List-Unsubscribe" header of an email 61 | * @param {string} rawContent - The raw email content 62 | * @return {string|null} - The URL specified in the "List-Unsubscribe" header, or null if the header is not found 63 | */ 64 | function RawListUnsubscribe(rawContent) { 65 | var value = rawContent.match(/^List-Unsubscribe: ((.|\r\n\s)+)\r\n/m); 66 | //Logger.log(value); 67 | if (value !== null) { 68 | var url = value[1].match(/https?:\/\/[^>]+/)[0]; 69 | return url; 70 | } 71 | return null; 72 | } 73 | 74 | /** 75 | * Attempts to extract an unsubscribe link from the body of an email 76 | * @param {string} body - The body of the email 77 | * @return {string|null} - The unsubscribe link, or null if no link is found 78 | */ 79 | function getEmailUnsubscribeLink(body) { 80 | // Search for the word "Unsubscribe" 81 | var unsubscribeIndex = body.toLowerCase().indexOf("unsubscribe"); 82 | if (unsubscribeIndex != -1) { 83 | // Check if the word "Unsubscribe" is a link 84 | var linkRegex = /]*?\s+)?href=(["'])(.*?)\1/; 85 | var match = body.substring(unsubscribeIndex).match(linkRegex); 86 | if (match != null) { 87 | var unsubscribeLink = match[2]; 88 | // Return the link 89 | return unsubscribeLink; 90 | } 91 | } 92 | // If no link was found, return null 93 | return null; 94 | } 95 | 96 | /** 97 | * Follows an unsubscribe link 98 | */ 99 | function followUnsubscribeLink(link, message) { 100 | var options = { 101 | followRedirects: false, 102 | muteHttpExceptions: true 103 | }; 104 | if (link && (link.startsWith("http://") || link.startsWith("https://"))) { 105 | var response = UrlFetchApp.fetch(link, options); 106 | var content = response.getContentText(); 107 | return checkUnsubscribeSuccess(content, message); 108 | } 109 | return false; 110 | } 111 | 112 | 113 | // This function checks if the unsubscribe process was successful by looking for 114 | // success phrases in the content of the webpage resulting from following the unsubscribe link 115 | function checkUnsubscribeSuccess(content) { 116 | // List of common success phrases used by websites to confirm a successful unsubscribe 117 | var successPhrases = [ 118 | "you have been unsubscribed", 119 | "unsubscription confirmed", 120 | "successfully unsubscribed", 121 | "your email has been removed", 122 | "you've been removed from our list", 123 | "opt-out successful", 124 | "you will no longer receive emails from us", 125 | "we're sorry to see you go", 126 | "your subscription has been canceled", 127 | "your request has been processed", 128 | ]; 129 | 130 | // Initialize a variable to store the result of the unsubscribe process 131 | var success = false; 132 | 133 | // Attempt to parse the HTML content using XmlService 134 | var htmlDocument; 135 | try { 136 | htmlDocument = XmlService.parse(content); 137 | } catch (error) { 138 | // If parsing fails, return false (unsuccessful unsubscribe) 139 | return success; 140 | } 141 | 142 | // Get the root node of the parsed HTML document 143 | var rootNode = htmlDocument.getRootElement(); 144 | 145 | // Get all text nodes from the HTML document 146 | var allTextNodes = getTextNodes(rootNode, []); 147 | 148 | // Loop through all text nodes 149 | allTextNodes.forEach(function (textNode) { 150 | // Get the text content of the node and convert it to lowercase 151 | var textContent = textNode.getText().toLowerCase(); 152 | 153 | // Loop through the success phrases 154 | successPhrases.forEach(function (phrase) { 155 | // If the text content includes a success phrase, set success to true 156 | if (textContent.includes(phrase)) { 157 | success = true; 158 | } 159 | }); 160 | }); 161 | 162 | // Return the result of the unsubscribe process (true if successful, false if not) 163 | return success; 164 | } 165 | 166 | // This function recursively retrieves all text nodes from a given node and its children 167 | function getTextNodes(node, textNodes) { 168 | // If the node is a text node, add it to the textNodes array 169 | if (node.getType() === XmlService.ContentTypes.TEXT) { 170 | textNodes.push(node); 171 | } 172 | // If the node is an element node, process its children 173 | else if (node.getType() === XmlService.ContentTypes.ELEMENT) { 174 | var children = node.getChildren(); 175 | for (var i = 0; i < children.length; i++) { 176 | getTextNodes(children[i], textNodes); 177 | } 178 | } 179 | // Return the updated textNodes array 180 | return textNodes; 181 | } --------------------------------------------------------------------------------