You’re using Python’s imaplib to search emails, but mail.search() keeps returning empty results even though you know matching emails exist. Here’s why it happens and how to fix it.
The Problem#
1
2
3
4
5
6
7
8
9
| import imaplib
mail = imaplib.IMAP4_SSL('imap.gmail.com')
mail.login('user@gmail.com', 'app-password')
mail.select('INBOX')
# This returns nothing!
status, messages = mail.search(None, 'FROM "john@example.com" SUBJECT "invoice"')
print(messages) # [b'']
|
You’ve verified the emails exist. You can see them in Gmail. But search() returns an empty byte string.
Common Causes and Fixes#
1. Wrong Search Syntax (Most Common)#
IMAP search syntax is specific and unforgiving. The search string you pass to mail.search() is sent directly to the IMAP server — it’s not Python string matching.
Wrong:
1
2
| # Combining criteria incorrectly
mail.search(None, 'FROM "john" SUBJECT "invoice"')
|
Right:
1
2
| # Each criterion is a separate argument, or use parentheses
mail.search(None, '(FROM "john" SUBJECT "invoice")')
|
For OR conditions, use the OR keyword before the criteria:
1
2
3
4
5
| # Wrong - this won't work
mail.search(None, 'FROM "john" OR FROM "jane"')
# Right - OR precedes the two criteria it joins
mail.search(None, 'OR FROM "john" FROM "jane"')
|
2. Searching the Wrong Folder#
Gmail uses [Gmail]/All Mail for everything, but INBOX only contains emails actually in your inbox (not archived).
1
2
3
4
5
6
7
| # Only searches inbox
mail.select('INBOX')
# Searches all mail including archived
mail.select('"[Gmail]/All Mail"')
# Note the quotes inside quotes - required for folder names with spaces/special chars
|
List available folders to see what’s there:
1
2
3
| status, folders = mail.list()
for folder in folders:
print(folder.decode())
|
3. Case Sensitivity Issues#
IMAP servers vary in how they handle case sensitivity. Gmail is case-insensitive for most searches, but other servers may not be.
1
2
3
4
5
| # May not match "John@Example.com"
mail.search(None, 'FROM "john@example.com"')
# Search the body instead for more flexibility
mail.search(None, 'BODY "john"')
|
IMAP dates must be in a specific format: DD-Mon-YYYY
1
2
3
4
5
6
| # Wrong
mail.search(None, 'SINCE "2026-03-01"')
mail.search(None, 'SINCE "03/01/2026"')
# Right
mail.search(None, 'SINCE "01-Mar-2026"')
|
5. TEXT vs BODY vs Subject/From#
TEXT searches everything (headers + body)BODY searches only the message bodyFROM, SUBJECT, etc. search specific headers
1
2
3
4
5
6
7
8
| # Search everywhere for a string
mail.search(None, 'TEXT "project update"')
# Search only the body
mail.search(None, 'BODY "project update"')
# Search only subject line
mail.search(None, 'SUBJECT "project update"')
|
Complete Working Example#
Here’s a robust search function that handles common edge cases:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| import imaplib
import email
from email.header import decode_header
def search_emails(credentials, search_criteria, folder='"[Gmail]/All Mail"'):
"""
Search emails with proper error handling.
Args:
credentials: dict with 'email', 'password', 'server'
search_criteria: IMAP search string, e.g., '(FROM "john" SUBJECT "invoice")'
folder: IMAP folder to search
Returns:
List of message IDs
"""
mail = imaplib.IMAP4_SSL(credentials.get('server', 'imap.gmail.com'))
try:
mail.login(credentials['email'], credentials['password'])
# Select folder - returns count of messages
status, data = mail.select(folder, readonly=True)
if status != 'OK':
print(f"Failed to select folder: {folder}")
return []
# Perform search
status, messages = mail.search(None, search_criteria)
if status != 'OK':
print(f"Search failed: {status}")
return []
# messages[0] is a space-separated byte string of IDs
msg_ids = messages[0].split()
return msg_ids
except imaplib.IMAP4.error as e:
print(f"IMAP error: {e}")
return []
finally:
try:
mail.logout()
except:
pass
# Usage
creds = {
'email': 'you@gmail.com',
'password': 'your-app-password',
'server': 'imap.gmail.com'
}
# Search with proper syntax
results = search_emails(creds, '(FROM "client@example.com" SINCE "01-Mar-2026")')
print(f"Found {len(results)} emails")
|
IMAP Search Criteria Quick Reference#
| Criteria | Example | Meaning |
|---|
ALL | ALL | All messages |
UNSEEN | UNSEEN | Unread messages |
FROM | FROM "john" | From contains “john” |
TO | TO "me@example.com" | To contains address |
SUBJECT | SUBJECT "meeting" | Subject contains “meeting” |
BODY | BODY "keyword" | Body contains “keyword” |
TEXT | TEXT "keyword" | Anywhere in message |
SINCE | SINCE "01-Mar-2026" | After date |
BEFORE | BEFORE "31-Mar-2026" | Before date |
FLAGGED | FLAGGED | Starred/flagged |
OR | OR FROM "a" FROM "b" | Either condition |
NOT | NOT SEEN | Negation |
Still Getting Empty Results?#
- Test with a broader search first:
mail.search(None, 'ALL') — if this returns nothing, you’re in the wrong folder - Check folder names: Use
mail.list() to see exact folder names - Verify credentials: Wrong app password = silent failures on some servers
- Try the Gmail API instead: For complex searches, Gmail’s API is more reliable than IMAP
The IMAP protocol is old and quirky, but once you understand the syntax rules, it works reliably. The key is remembering that the search string is passed directly to the server — Python doesn’t interpret it.