gmao/app_new/outlook/sync.py

357 lines
14 KiB
Python
Raw Normal View History

2026-08-14 18:02:25 +02:00
"""Outlook Sync Routes - GMAO Collège"""
from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify, send_file
from flask_login import login_required, current_user
from app_new.extensions import db
from ..outlook.models import OutlookAccount, OutlookMail, OutlookFolder, OutlookAttachment, OutlookMailInterpretation, GmaoContext
from ..outlook.auth import get_access_token
import os
import subprocess
import json
import tempfile
sync_bp = Blueprint('outlook_sync', __name__, template_folder='templates')
@sync_bp.route('/api/sync-folders/<int:account_id>', methods=['POST'])
2026-08-14 18:02:25 +02:00
@login_required
def api_sync_folders(account_id):
"""Synchronise les dossiers Outlook (avec sous-dossiers récursifs)."""
import requests
from datetime import datetime, timezone
account = OutlookAccount.query.get(account_id)
if not account or (account.user_id != current_user.id and not current_user.is_admin()):
2026-08-14 18:02:25 +02:00
return jsonify({'error': 'Compte non trouvé'}), 404
access_token, error = get_access_token(account)
if error:
return jsonify({'error': error}), 401
try:
headers = {'Authorization': f'Bearer {access_token}'}
# Fonction récursive pour récupérer les dossiers
def fetch_folders(parent_id=None, depth=0):
if depth > 5: # Limiter la profondeur
return []
folders = []
if parent_id:
url = f'https://outlook.office365.com/api/v2.0/me/MailFolders/{parent_id}/ChildFolders'
else:
url = 'https://outlook.office365.com/api/v2.0/me/MailFolders'
while url:
resp = requests.get(url, headers=headers)
if resp.status_code != 200:
break
data = resp.json()
for folder_data in data.get('value', []):
folders.append(folder_data)
# Récupérer les sous-dossiers récursivement
child_folders = fetch_folders(folder_data.get('Id'), depth + 1)
folders.extend(child_folders)
url = data.get('@odata.nextLink')
return folders
all_folders = fetch_folders()
synced = 0
for folder_data in all_folders:
folder_id = folder_data.get('Id')
existing = OutlookFolder.query.filter_by(id=folder_id).first()
if not existing:
folder_name = folder_data.get('DisplayName') or folder_data.get('Name') or 'Inconnu'
folder_type = folder_data.get('WellKnownFolderName', 'custom')
default_folders = {
'inbox': 'Boîte de réception',
'sentitems': 'Éléments envoyés',
'drafts': 'Brouillons',
'deleteditems': 'Éléments supprimés',
'junkemail': 'Courrier indésirable',
'outbox': "Boîte d'envoi",
'archive': 'Archive'
}
display_name = default_folders.get(folder_type, folder_name)
folder = OutlookFolder(
id=folder_id,
account_id=account.id,
name=folder_name,
display_name=display_name,
parent_id=folder_data.get('ParentFolderId'),
total_items=folder_data.get('TotalItemCount', 0),
unread_items=folder_data.get('UnreadItemCount', 0),
folder_type=folder_type
)
db.session.add(folder)
synced += 1
else:
existing.total_items = folder_data.get('TotalItemCount', 0)
existing.unread_items = folder_data.get('UnreadItemCount', 0)
folder_name = folder_data.get('DisplayName') or folder_data.get('Name') or existing.name
existing.name = folder_name
folder_type = folder_data.get('WellKnownFolderName', 'custom')
default_folders = {
'inbox': 'Boîte de réception',
'sentitems': 'Éléments envoyés',
'drafts': 'Brouillons',
'deleteditems': 'Éléments supprimés',
'junkemail': 'Courrier indésirable',
'outbox': "Boîte d'envoi",
'archive': 'Archive'
}
existing.display_name = default_folders.get(folder_type, folder_name)
db.session.commit()
return jsonify({
'success': True,
'message': f'{synced} nouveaux dossiers synchronisés',
'total': len(all_folders)
})
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({'error': f'Erreur: {str(e)}'}), 500
@sync_bp.route('/api/sync-mails/<int:account_id>/<folder_id>', methods=['POST'])
@sync_bp.route('/api/sync-mails/<int:account_id>', defaults={'folder_id': 'inbox'}, methods=['POST'])
2026-08-14 18:02:25 +02:00
@login_required
def api_sync_mails(account_id, folder_id='inbox'):
"""Synchronise les mails d'un dossier avec pagination complète."""
import requests
from datetime import datetime, timezone
account = OutlookAccount.query.get(account_id)
if not account or (account.user_id != current_user.id and not current_user.is_admin()):
2026-08-14 18:02:25 +02:00
return jsonify({'error': 'Compte non trouvé'}), 404
access_token, error = get_access_token(account)
if error:
return jsonify({'error': error}), 401
# Paramètres de pagination
max_messages = int(request.args.get('max', 200)) # Max messages à récupérer
try:
# Trouver le dossier
folder = None
if folder_id != 'inbox':
folder = OutlookFolder.query.filter_by(id=folder_id, account_id=account_id).first()
2026-08-14 18:02:25 +02:00
else:
# Chercher par folder_type ou par nom (Boîte de réception)
folder = OutlookFolder.query.filter_by(account_id=account_id, folder_type='inbox').first()
if not folder:
folder = OutlookFolder.query.filter(
OutlookFolder.account_id == account_id,
OutlookFolder.name.like('%ception%')
).first()
if not folder:
return jsonify({'error': 'Dossier non trouvé'}), 404
headers = {'Authorization': f'Bearer {access_token}'}
# Récupérer tous les messages avec pagination
all_mails = []
url = f'https://outlook.office365.com/api/v2.0/me/MailFolders/{folder.id}/messages?$top=100&$orderby=ReceivedDateTime desc&$select=Id,Subject,From,ReceivedDateTime,IsRead,HasAttachments,BodyPreview,Body,ToRecipients,CcRecipients,Importance'
while url and len(all_mails) < max_messages:
resp = requests.get(url, headers=headers)
if resp.status_code != 200:
error_data = resp.json()
error_msg = error_data.get('error', {}).get('message', resp.text) if isinstance(error_data, dict) else resp.text
return jsonify({'error': f'Erreur API Outlook: {error_msg}'}), 500
data = resp.json()
mails_data = data.get('value', [])
all_mails.extend(mails_data)
# Vérifier s'il y a une page suivante
url = data.get('@odata.nextLink')
# Arrêter si on a atteint la limite
if len(all_mails) >= max_messages:
break
saved_count = 0
updated_count = 0
for mail_data in all_mails:
mail_id = mail_data.get('Id')
existing = OutlookMail.query.filter_by(id=mail_id).first()
from_addr = mail_data.get('From', {}).get('EmailAddress', {})
body = mail_data.get('Body', {})
received_str = mail_data.get('ReceivedDateTime', '')
try:
received_at = datetime.fromisoformat(received_str.replace('Z', '+00:00'))
except:
received_at = datetime.now(timezone.utc)
# Destinataires
import json
to_recipients = json.dumps([r.get('EmailAddress', {}).get('Address', '') for r in mail_data.get('ToRecipients', [])])
cc_recipients = json.dumps([r.get('EmailAddress', {}).get('Address', '') for r in mail_data.get('CcRecipients', [])])
if not existing:
mail = OutlookMail(
id=mail_id,
account_id=account.id,
folder_id=folder.id,
subject=mail_data.get('Subject', ''),
from_name=from_addr.get('Name', ''),
from_email=from_addr.get('Address', ''),
body_preview=mail_data.get('BodyPreview', ''),
body_content=body.get('Content', ''),
body_type=body.get('ContentType', 'text'),
received_at=received_at,
is_read=mail_data.get('IsRead', False),
importance=mail_data.get('Importance', 'normal'),
has_attachments=mail_data.get('HasAttachments', False),
to_recipients=to_recipients,
cc_recipients=cc_recipients
)
db.session.add(mail)
saved_count += 1
else:
# Mettre à jour le statut de lecture
if existing.is_read != mail_data.get('IsRead', False):
existing.is_read = mail_data.get('IsRead', False)
updated_count += 1
# Mettre à jour les infos du dossier
folder.total_items = len(all_mails)
folder.unread_items = sum(1 for m in all_mails if not m.get('IsRead', False))
account.last_connected_at = datetime.now(timezone.utc)
db.session.commit()
return jsonify({
'success': True,
'message': f'{saved_count} nouveaux mails, {updated_count} mis à jour sur {len(all_mails)} récupérés',
'total': len(all_mails),
'new': saved_count,
'updated': updated_count
})
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({'error': f'Erreur: {str(e)}'}), 500
# ─── Pièces jointes ─────────────────────────────────────────────────────────
@sync_bp.route('/api/sync-attachments/<mail_id>', methods=['POST'])
2026-08-14 18:02:25 +02:00
@login_required
def api_sync_attachments(mail_id):
"""Synchronise les pièces jointes d'un email."""
import requests
mail = OutlookMail.query.get_or_404(mail_id)
account = OutlookAccount.query.get_or_404(mail.account_id)
if account.user_id != current_user.id and not current_user.is_admin():
return jsonify({'error': 'Message non trouvé'}), 404
2026-08-14 18:02:25 +02:00
access_token, error = get_access_token(account)
if error:
return jsonify({'error': error}), 401
try:
headers = {'Authorization': f'Bearer {access_token}'}
resp = requests.get(
f'https://outlook.office365.com/api/v2.0/me/messages/{mail_id}/attachments',
headers=headers
)
if resp.status_code != 200:
return jsonify({'error': f'Erreur API: {resp.text}'}), 500
attachments_data = resp.json().get('value', [])
synced = 0
for att_data in attachments_data:
att_id = att_data.get('Id')
existing = OutlookAttachment.query.filter_by(id=att_id).first()
if not existing:
attachment = OutlookAttachment(
id=att_id,
mail_id=mail_id,
name=att_data.get('Name', 'sans_nom'),
content_type=att_data.get('ContentType'),
size=att_data.get('Size', 0),
is_inline=att_data.get('IsInline', False),
content_id=att_data.get('ContentId')
)
db.session.add(attachment)
synced += 1
db.session.commit()
return jsonify({
'success': True,
'message': f'{synced} nouvelles pièces jointes',
'total': len(attachments_data)
})
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({'error': f'Erreur: {str(e)}'}), 500
@sync_bp.route('/attachment/<att_id>/download')
@login_required
def download_attachment(att_id):
"""Télécharge une pièce jointe."""
import requests
attachment = OutlookAttachment.query.get_or_404(att_id)
mail = OutlookMail.query.get_or_404(attachment.mail_id)
account = OutlookAccount.query.get_or_404(mail.account_id)
if account.user_id != current_user.id and not current_user.is_admin():
return jsonify({'error': 'Pièce jointe non trouvée'}), 404
2026-08-14 18:02:25 +02:00
access_token, error = get_access_token(account)
if error:
flash(error, 'danger')
return redirect(url_for('outlook_pages.mail_view', account_id=account.id, mail_id=mail.id))
try:
headers = {'Authorization': f'Bearer {access_token}'}
resp = requests.get(
f'https://outlook.office365.com/api/v2.0/me/messages/{mail.id}/attachments/{att_id}/$value',
headers=headers,
stream=True
)
if resp.status_code != 200:
flash('Erreur lors du téléchargement', 'danger')
return redirect(url_for('outlook_pages.mail_view', account_id=account.id, mail_id=mail.id))
# Sauvegarder temporairement
import tempfile
temp_dir = tempfile.gettempdir()
temp_path = os.path.join(temp_dir, attachment.name)
with open(temp_path, 'wb') as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
return send_file(temp_path, as_attachment=True, download_name=attachment.name)
except Exception as e:
flash(f'Erreur: {str(e)}', 'danger')
return redirect(url_for('outlook_pages.mail_view', account_id=account.id, mail_id=mail.id))