Issues (6)

app/getemail.py (2 issues)

1
#!/usr/bin/env python3
2
"""
3
Email balance import script for pycashflow.
4
Processes IMAP email inboxes to extract and import balance information.
5
6
This script can be run standalone from cron or called from within the application.
7
"""
8
import os
9
import sys
10
11
# When run as standalone script, add parent directory to path for imports
12
# This allows 'from app import ...' to work when called from cron
13
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
14
if parent_dir not in sys.path:
15
    sys.path.insert(0, parent_dir)
16
17
import imaplib
18
import email
19
from email.header import decode_header
20
from email.mime.text import MIMEText
21
from email.mime.multipart import MIMEMultipart
22
import smtplib
23
from datetime import datetime, timedelta
24
from app import db
25
from app.models import User, Email, Balance, GlobalEmailSettings
26
27
28
def process_email_balances():
29
    """
30
    Process email inboxes for all users with email settings configured
31
    and update their balance information from bank emails.
32
33
    This function supports both SQLite and PostgreSQL through SQLAlchemy.
34
    """
35
    # OUTER LOOP: Get all users with email settings configured
36
    users_with_email = db.session.query(User, Email).join(
37
        Email, User.id == Email.user_id
38
    ).all()
39
40
    for user, email_config in users_with_email:
41
        user_id = user.id
42
        username = email_config.email
43
        password = email_config.password
44
        imap_server = email_config.server
45
        subjectstr = email_config.subjectstr
46
        startstr = email_config.startstr
47
        endstr = email_config.endstr
48
49
        try:
50
            # Create IMAP connection for THIS user
51
            imap = imaplib.IMAP4_SSL(imap_server)
52
            imap.login(username, password)
53
54
            status, messages = imap.select("INBOX", readonly=True)
55
56
            # Filter for emails from the past day to limit inbox processing
57
            # IMAP SINCE searches for messages with Date on or after the specified date
58
            yesterday = (datetime.now() - timedelta(days=1)).strftime("%d-%b-%Y")
59
            status, message_ids = imap.search(None, f'SINCE {yesterday}')
60
61
            # Parse message IDs from the search result
62
            if message_ids[0]:
63
                email_ids = message_ids[0].split()
64
            else:
65
                email_ids = []
66
67
            print(f"Found {len(email_ids)} email(s) from the past day for user {user_id}")
68
69
            email_content = {}
70
71
            # INNER LOOP: Process only emails from the past day
72
            for email_id in email_ids:
73
                try:
74
                    res, msg = imap.fetch(email_id, "(RFC822)")
75
                    for response in msg:
76
                        if isinstance(response, tuple):
77
                            msg = email.message_from_bytes(response[1])
78
79
                            # Decode subject
80
                            subject, encoding = decode_header(msg["Subject"])[0]
81
                            if isinstance(subject, bytes):
82
                                try:
83
                                    subject = subject.decode(encoding)
84
                                except:
85
                                    subject = "subject"
86
87
                            # Decode sender
88
                            From, encoding = decode_header(msg.get("From"))[0]
89
                            if isinstance(From, bytes):
90
                                try:
91
                                    From = From.decode(encoding)
92
                                except:
93
                                    pass
94
95
                            # Extract email body
96
                            if msg.is_multipart():
97
                                for part in msg.walk():
98
                                    content_type = part.get_content_type()
99
                                    content_disposition = str(part.get("Content-Disposition"))
100
                                    try:
101
                                        body = part.get_payload(decode=True).decode()
102
                                    except:
103
                                        pass
104
                                    if content_type == "text/plain" and "attachment" not in content_disposition:
105
                                        try:
106
                                            email_content[subject] = body
107
                                        except:
108
                                            pass
109
                            else:
110
                                content_type = msg.get_content_type()
111
                                body = msg.get_payload(decode=True).decode()
112
                                if content_type == "text/plain":
113
                                    try:
114
                                        email_content[subject] = body
115
                                    except:
116
                                        pass
117
                except:
118
                    pass  # Skip individual email errors
119
120
            # Extract balance from emails for THIS user
121
            try:
122
                start_index = email_content[subjectstr].find(startstr) + len(startstr)
123
                end_index = email_content[subjectstr].find(endstr)
124
                new_balance = email_content[subjectstr][start_index:end_index].replace(',', '')
125
                new_balance = new_balance.replace('$', '')
126
                new_balance = float(new_balance)
127
128
                # Insert balance WITH user_id using SQLAlchemy ORM
129
                balance = Balance(
130
                    amount=new_balance,
131
                    date=datetime.today().date(),
132
                    user_id=user_id
133
                )
134
                db.session.add(balance)
135
                db.session.commit()
136
                print(f"Successfully imported balance ${new_balance} for user {user_id}")
137
            except KeyError:
138
                # No email with the specified subject found
139
                pass
140
            except Exception as e:
141
                # No balance found in emails for this user
142
                print(f"Could not extract balance for user {user_id}: {e}")
143
144
            # Close IMAP connection for THIS user
145
            imap.close()
146
            imap.logout()
147
148
        except Exception as e:
149
            # Failed to connect to IMAP for this user - skip to next user
150
            print(f"Failed to process emails for user {user_id}: {e}")
151
            continue
152
153
154 View Code Duplication
def send_new_user_notification(new_user_name, new_user_email):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
155
    """
156
    Send an email notification to the global admin when a new user registers.
157
158
    Args:
159
        new_user_name: Name of the newly registered user
160
        new_user_email: Email of the newly registered user
161
162
    Returns:
163
        bool: True if email was sent successfully, False otherwise
164
    """
165
    try:
166
        # Get the global admin user
167
        global_admin = User.query.filter_by(is_global_admin=True).first()
168
169
        if not global_admin:
170
            print("No global admin found, skipping notification")
171
            return False
172
173
        # Get global email settings
174
        email_settings = GlobalEmailSettings.query.first()
175
176
        if not email_settings:
177
            print("No global email settings configured, skipping notification")
178
            return False
179
180
        # Extract email credentials from global settings
181
        from_email = email_settings.email
182
        password = email_settings.password
183
        smtp_server = email_settings.smtp_server
184
185
        # Create the email message
186
        msg = MIMEMultipart('alternative')
187
        msg['From'] = from_email
188
        msg['To'] = global_admin.email  # Send to the global admin's email
189
        msg['Subject'] = 'New User Registration - PyCashFlow'
190
191
        # Create plain text version
192
        text_content = f"""
193
A new user has registered on PyCashFlow.
194
195
Name: {new_user_name}
196
Email: {new_user_email}
197
Registration Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
198
199
This user account is currently inactive and requires approval before they can log in.
200
Please log in to your PyCashFlow account to activate this user.
201
"""
202
203
        # Create HTML version
204
        html_content = f"""
205
<html>
206
  <body>
207
    <h2>New User Registration</h2>
208
    <p>A new user has registered on PyCashFlow.</p>
209
    <table style="border-collapse: collapse; margin-top: 10px;">
210
      <tr>
211
        <td style="padding: 5px; font-weight: bold;">Name:</td>
212
        <td style="padding: 5px;">{new_user_name}</td>
213
      </tr>
214
      <tr>
215
        <td style="padding: 5px; font-weight: bold;">Email:</td>
216
        <td style="padding: 5px;">{new_user_email}</td>
217
      </tr>
218
      <tr>
219
        <td style="padding: 5px; font-weight: bold;">Registration Date:</td>
220
        <td style="padding: 5px;">{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</td>
221
      </tr>
222
    </table>
223
    <p style="margin-top: 15px;">
224
      <strong>Note:</strong> This user account is currently inactive and requires approval before they can log in.
225
      Please log in to your PyCashFlow account to activate this user.
226
    </p>
227
  </body>
228
</html>
229
"""
230
231
        # Attach both versions
232
        part1 = MIMEText(text_content, 'plain')
233
        part2 = MIMEText(html_content, 'html')
234
        msg.attach(part1)
235
        msg.attach(part2)
236
237
        # Send the email using SMTP
238
        # Try port 587 with STARTTLS first (most common)
239
        try:
240
            server = smtplib.SMTP(smtp_server, 587, timeout=10)
241
            server.starttls()
242
            server.login(from_email, password)
243
            server.sendmail(from_email, global_admin.email, msg.as_string())
244
            server.quit()
245
            print(f"Successfully sent new user notification email to {global_admin.email}")
246
            return True
247
        except Exception as e:
248
            # If port 587 fails, try port 465 with SSL
249
            print(f"Failed to send via port 587, trying SSL on port 465: {e}")
250
            try:
251
                server = smtplib.SMTP_SSL(smtp_server, 465, timeout=10)
252
                server.login(from_email, password)
253
                server.sendmail(from_email, global_admin.email, msg.as_string())
254
                server.quit()
255
                print(f"Successfully sent new user notification email to {global_admin.email} via SSL")
256
                return True
257
            except Exception as e2:
258
                print(f"Failed to send email via both ports: {e2}")
259
                return False
260
261
    except Exception as e:
262
        print(f"Error sending new user notification: {e}")
263
        return False
264
265
266 View Code Duplication
def send_account_activation_notification(user_name, user_email):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
267
    """
268
    Send an email notification to a user when their account is activated.
269
270
    Args:
271
        user_name: Name of the user whose account was activated
272
        user_email: Email of the user whose account was activated
273
274
    Returns:
275
        bool: True if email was sent successfully, False otherwise
276
    """
277
    try:
278
        # Get global email settings
279
        email_settings = GlobalEmailSettings.query.first()
280
281
        if not email_settings:
282
            print("No global email settings configured, skipping activation notification")
283
            return False
284
285
        # Extract email credentials from global settings
286
        from_email = email_settings.email
287
        password = email_settings.password
288
        smtp_server = email_settings.smtp_server
289
290
        # Create the email message
291
        msg = MIMEMultipart('alternative')
292
        msg['From'] = from_email
293
        msg['To'] = user_email
294
        msg['Subject'] = 'Your PyCashFlow Account Has Been Activated'
295
296
        # Create plain text version
297
        text_content = f"""
298
Hello {user_name},
299
300
Great news! Your PyCashFlow account has been activated and you can now log in.
301
302
You can access your account at any time by visiting the login page.
303
304
If you have any questions or need assistance, please contact your administrator.
305
306
Best regards,
307
The PyCashFlow Team
308
"""
309
310
        # Create HTML version
311
        html_content = f"""
312
<html>
313
  <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
314
    <div style="max-width: 600px; margin: 0 auto; padding: 20px;">
315
      <h2 style="color: #10b981; border-bottom: 3px solid #10b981; padding-bottom: 10px;">
316
        Account Activated!
317
      </h2>
318
      <p>Hello <strong>{user_name}</strong>,</p>
319
      <p>Great news! Your PyCashFlow account has been activated and you can now log in.</p>
320
321
      <div style="background-color: #f0fdf4; border-left: 4px solid #10b981; padding: 15px; margin: 20px 0;">
322
        <p style="margin: 0;">
323
          <strong>What's next?</strong><br>
324
          You can now access all features of PyCashFlow including:
325
        </p>
326
        <ul style="margin: 10px 0;">
327
          <li>Cash flow forecasting</li>
328
          <li>Schedule management</li>
329
          <li>Balance tracking</li>
330
          <li>And more!</li>
331
        </ul>
332
      </div>
333
334
      <p>If you have any questions or need assistance, please contact your administrator.</p>
335
336
      <p style="margin-top: 30px;">
337
        Best regards,<br>
338
        <strong>The PyCashFlow Team</strong>
339
      </p>
340
    </div>
341
  </body>
342
</html>
343
"""
344
345
        # Attach both versions
346
        part1 = MIMEText(text_content, 'plain')
347
        part2 = MIMEText(html_content, 'html')
348
        msg.attach(part1)
349
        msg.attach(part2)
350
351
        # Send the email using SMTP
352
        # Try port 587 with STARTTLS first (most common)
353
        try:
354
            server = smtplib.SMTP(smtp_server, 587, timeout=10)
355
            server.starttls()
356
            server.login(from_email, password)
357
            server.sendmail(from_email, user_email, msg.as_string())
358
            server.quit()
359
            print(f"Successfully sent account activation email to {user_email}")
360
            return True
361
        except Exception as e:
362
            # If port 587 fails, try port 465 with SSL
363
            print(f"Failed to send via port 587, trying SSL on port 465: {e}")
364
            try:
365
                server = smtplib.SMTP_SSL(smtp_server, 465, timeout=10)
366
                server.login(from_email, password)
367
                server.sendmail(from_email, user_email, msg.as_string())
368
                server.quit()
369
                print(f"Successfully sent account activation email to {user_email} via SSL")
370
                return True
371
            except Exception as e2:
372
                print(f"Failed to send email via both ports: {e2}")
373
                return False
374
375
    except Exception as e:
376
        print(f"Error sending account activation notification: {e}")
377
        return False
378
379
380
# Allow script to be run standalone from cron
381
if __name__ == "__main__":
382
    from app import create_app
383
384
    print("Starting email balance import...")
385
    app = create_app()
386
387
    with app.app_context():
388
        try:
389
            process_email_balances()
390
            print("Email balance import completed successfully")
391
        except Exception as e:
392
            print(f"Email balance import failed: {e}")
393
            sys.exit(1)
394