diff --git a/AutoUnsubscriber.py b/AutoUnsubscriber.py index 8ddc46b..eaeda4d 100644 --- a/AutoUnsubscriber.py +++ b/AutoUnsubscriber.py @@ -7,20 +7,63 @@ import webbrowser import re import sys +import ssl '''List of accepted service providers and respective imap link''' servers = [('Gmail','imap.gmail.com'),('Outlook','imap-mail.outlook.com'), ('Hotmail','imap-mail.outlook.com'),('Yahoo','imap.mail.yahoo.com'), ('ATT','imap.mail.att.net'),('Comcast','imap.comcast.net'), ('Verizon','incoming.verizon.net'),('AOL','imap.aol.com'), - ('Zoho','imap.zoho.com')] + ('Zoho','imap.zoho.com'),('GMX','imap.gmx.com'),('ProtonMail','127.0.0.1')] +#Rewrote with dictionaries +serverD = { + 'Gmail': { + 'imap': 'imap.gmail.com', + 'domains': ['@gmail.com'] + }, + 'Outlook/Hotmail': { + 'imap': 'imap-mail.outlook.com', + 'domains': ['@outlook.com','@hotmail.com'] + }, + 'Yahoo': { + 'imap': 'imap.mail.yahoo.com', + 'domains': ['@yahoo.com'] + }, + 'ATT': { + 'imap': 'imap.mail.att.net', + 'domains': ['@att.net'] + }, + 'Comcast': { + 'imap': 'imap.comcast.net', + 'domains': ['@comcast.net'] + }, + 'Verizon': { + 'imap': 'incoming.verizon.net', + 'domains': ['@verizon.net'] + }, + 'AOL': { + 'imap': 'imap.aol.com', + 'domains': ['@aol.com'] + }, + 'Zoho': { + 'imap': 'imap.zoho.com', + 'domains': ['@zoho.com'] + }, + 'GMX': { + 'imap': 'imap.gmx.com', + 'domains': ['@gmx.com'] + }, + 'ProtonMail': { + 'imap': '127.0.0.1', + 'domains': ['@protonmail.com','@pm.me'] + } +} + -#add to words if more words found '''Key words for unsubscribe link - add more if found''' words = ['unsubscribe','subscription','optout'] class AutoUnsubscriber(): - def __init__(self): self.email = '' self.user = None @@ -30,37 +73,54 @@ def __init__(self): self.delEmails = False self.senderList = [] self.noLinkList = [] - self.wordCheck = [] self.providers = [] - for i in range(len(servers)): - self.providers.append(re.compile(servers[i][0], re.I)) - for i in range(len(words)): - self.wordCheck.append(re.compile(words[i], re.I)) - + #server name is later matched against second level domain names + for server in servers: + self.providers.append(re.compile(server[0], re.I)) + #TODO maybe add support for servers with a + #company name different than their domain name... '''Get initial user info - email, password, and service provider''' def getInfo(self): print('This program searchs your email for junk mail to unsubscribe from and delete') - print('Suported emails: Gmail, Outlook, Hotmail, Yahoo, AOL, Zoho,') - print('AT&T, Comcast, and Verizon') + print('Supported emails: Gmail, Outlook, Hotmail, Yahoo, AOL, Zoho,') + print('GMX, AT&T, Comcast, ProtonMail (Bridge), and Verizon') print('Please note: you may need to allow access to less secure apps') getEmail = True while getEmail: - self.email = input('\nEnter your email address: ') - for j in range(len(self.providers)): - choice = self.providers[j].search(self.email) - if choice != None: - self.user = servers[j] - print('\nLooks like you\'re using a '+self.user[0]+' account\n') + self.email = str.lower(input('\nEnter your email address: ')) + for prov in serverD: + match=False + for domain in serverD[prov]['domains']: + if domain in self.email: + print('\nLooks like you\'re using a '+prov+' account\n') + self.user = (prov, serverD[prov]['imap']) + getEmail = False + match = True + break + if match: break + if self.user is None: + print('\nEmail type not recognized, enter an imap server, or press enter to try a different email address:\n') + myimap = input('\n[myimapserver.tld] | [enter] : ') + if myimap: + self.user = ('Self-defined IMAP', myimap) + print('\nYou are using a '+self.user[0]+' account!\n') getEmail = False break - if self.user == None: - print('\nNo useable email type detected, try a different account') + print('\nTry a different account') self.password = getpass.getpass('Enter password for '+self.email+': ') '''Log in to IMAP server, argument determines whether readonly or not''' def login(self, read=True): try: - self.imap = imapclient.IMAPClient(self.user[1], ssl=True) + '''ProtonMail Bridge Support - Requires unverified STARTTLS and changing ports''' + if self.user[0]=='ProtonMail': + print("\nProtonMail require ProtonMail Bridge installed, make sure you've used the password Bridge gives you.") + self.context = ssl.create_default_context() + self.context.check_hostname = False + self.context.verify_mode = ssl.CERT_NONE + self.imap = imapclient.IMAPClient(self.user[1], port=1143, ssl=False) + self.imap.starttls(ssl_context=self.context) + else: self.imap = imapclient.IMAPClient(self.user[1], ssl=True) self.imap._MAXLINE = 10000000 self.imap.login(self.email, self.password) self.imap.select_folder('INBOX', readonly=read) @@ -85,12 +145,16 @@ def accessServer(self, readonly=True): ''' def getEmails(self): print('Getting emails with unsubscribe in the body\n') - UIDs = self.imap.search(['BODY unsubscribe']) + UIDs = self.imap.search([u'TEXT','unsubscribe']) raw = self.imap.fetch(UIDs, ['BODY[]']) print('Getting links and addresses\n') for UID in UIDs: - '''Get address and check if sender already in senderList''' - msg = pyzmail.PyzMessage.factory(raw[UID][b'BODY[]']) + '''If Body exists (resolves weird error with no body emails from Yahoo), then + Get address and check if sender already in senderList ''' + if b'BODY[]' in raw[UID]: msg = pyzmail.PyzMessage.factory(raw[UID][b'BODY[]']) + else: + print("Odd Email at UID: "+str(UID)+"; SKIPPING....") + continue sender = msg.get_addresses('from') trySender = True for spammers in self.senderList: @@ -107,27 +171,26 @@ def getEmails(self): print('Searching for unsubscribe link from '+str(senderName)) url = False '''Parse html for elements with anchor tags''' - if msg.html_part != None: - html = msg.html_part.get_payload().decode('utf-8') + if html_piece := msg.html_part: + html = html_piece.get_payload().decode('utf-8') soup = bs4.BeautifulSoup(html, 'html.parser') elems = soup.select('a') '''For each anchor tag, use regex to search for key words''' - for i in range(len(elems)): - for j in range(len(self.wordCheck)): - k = self.wordCheck[j].search(str(elems[i])) + elems.reverse() + #search starting at the bottom of email + for elem in elems: + for word in self.words: '''If one is found, get the url''' - if k != None: + if re.match( word, str(elem), re.IGNORECASE): print('Link found') - url = elems[i].get('href') + url = elem.get('href') break - if url != False: - break + if url: break '''If link found, add info to senderList format: (Name, email, link, go to link, delete emails) If no link found, add to noLinkList ''' - if url != False: - self.senderList.append([senderName, sender[0][1], url, False, False]) + if url: self.senderList.append([senderName, sender[0][1], url, False, False]) else: print('No link found') notInList = True @@ -204,8 +267,8 @@ def openLinks(self): counter = 0 '''Log back into IMAP servers, NOT in readonly mode, and delete emails from - selected providers. Note: only deleting emails with unsubscribe in the body. - Emails from provider without unsubscribe in the body will not be deleted. + selected providers. Note: Deletes all emails from unsubscribed sender. + Emails from provider without unsubscribe in the body will be deleted. ''' def deleteEmails(self): if self.delEmails != True: @@ -217,10 +280,10 @@ def deleteEmails(self): DelTotal = 0 for i in range(len(self.senderList)): if self.senderList[i][4] == True: - print('Searching for emails to delete from '+str(self.senderList[i][1])) - fromSender = 'FROM '+str(self.senderList[i][1]) - '''Search for unsubscribe in body from selected providers''' - DelUIDs = self.imap.search(['BODY unsubscribe', fromSender]) + sender=str(self.senderList[i][1]) + print('Searching for emails to delete from '+sender) + '''Search for UID from selected providers''' + DelUIDs = self.imap.search([u'FROM', sender]) DelCount = 0 for DelUID in DelUIDs: '''Delete emails from selected providers''' @@ -277,7 +340,7 @@ def nextMove(self): def fullProcess(self): self.accessServer() self.getEmails() - if self.senderList != []: + if self.senderList: self.decisions() self.openLinks() self.deleteEmails() diff --git a/README.md b/README.md new file mode 100644 index 0000000..7be88f0 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +### AutoUnsubscriber + +This program is an email auto-unsubscriber. Depending on your email provider and settings, it may require you to allow access to less secure apps. + +It uses IMAP to log into your email. From there, it goes through every email with "unsubscribe" in the body, parses the HTML, and uses regex to search through anchor tags for keywords that indicate an unsubscribe link (unsubscribe, optout, etc). If it finds a match, it grabs the href link and puts the address and link in a list. + +After the program has a list of emails and links, for each address in the list, it gives the user the option to navigate to the unsubscribe link and to delete emails with unsubscribe in the body from the sender. + +Once the program finishes going through the list, it gives the user the option to run the program again on the same email address, run it on a different email address, or quit the program. + +------ +### Python package Installation + +``` +pip install getpass4 +pip install bs4 +pip install IMAPclient +pip install regex +pip install os-sys +pip install syspath +pip install pyopenssl +pip install pyzmail +pip install pyzmail39 +``` + + + diff --git a/README.md~ b/README.md~ new file mode 100644 index 0000000..c4e39bc --- /dev/null +++ b/README.md~ @@ -0,0 +1,22 @@ +This program is an email auto-unsubscriber. Depending on your email provider and settings, it may require you to allow access to less secure apps. + +It uses IMAP to log into your email. From there, it goes through every email with "unsubscribe" in the body, parses the HTML, and uses regex to search through anchor tags for keywords that indicate an unsubscribe link (unsubscribe, optout, etc). If it finds a match, it grabs the href link and puts the address and link in a list. + +After the program has a list of emails and links, for each address in the list, it gives the user the option to navigate to the unsubscribe link and to delete emails with unsubscribe in the body from the sender. + +Once the program finishes going through the list, it gives the user the option to run the program again on the same email address, run it on a different email address, or quit the program. + +------ +# Python package Installation + +''' +pip install getpass4 +pip install bs4 +pip install IMAPclient +pip install regex +pip install os-sys +pip install syspath +pip install pyopenssl +pip install pyzmail +pip install pyzmail35 +''' diff --git a/README.txt b/README.txt index 5ecc878..c4e39bc 100644 --- a/README.txt +++ b/README.txt @@ -4,4 +4,19 @@ It uses IMAP to log into your email. From there, it goes through every email wit After the program has a list of emails and links, for each address in the list, it gives the user the option to navigate to the unsubscribe link and to delete emails with unsubscribe in the body from the sender. -Once the program finishes going through the list, it gives the user the option to run the program again on the same email address, run it on a different email address, or quit the program. \ No newline at end of file +Once the program finishes going through the list, it gives the user the option to run the program again on the same email address, run it on a different email address, or quit the program. + +------ +# Python package Installation + +''' +pip install getpass4 +pip install bs4 +pip install IMAPclient +pip install regex +pip install os-sys +pip install syspath +pip install pyopenssl +pip install pyzmail +pip install pyzmail35 +'''