Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 104 additions & 41 deletions AutoUnsubscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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'''
Expand Down Expand Up @@ -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()
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
```



22 changes: 22 additions & 0 deletions README.md~
Original file line number Diff line number Diff line change
@@ -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
'''
17 changes: 16 additions & 1 deletion README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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
'''