/* 5 octobre 2024 PRÉSENTATION ======================================================= Mon Smartphone est configuré pour checker les emails toutes les 10 minutes. Or j'ai besoin de plus de réactivité. Donc quand je travaille sur PC, cet exécutable 1 fois par minute se connecte au serveur POP3 en TLS et récupère les en-têtes SMTP des nouveaux messages, pour afficher uniquement les champs FROM et le SUBJECT et dire via synthèse vocale "Vous avez 3 nouveaux messages". S'il y a des nouveaux messages, j'ouvre peut-être un client email pour les voir, ou directement le site concerné.Ce programme de 26.2Ko compilé tourne à 0% CPU. La 1re version était en C console. Or une console occupe 17Mo de mémoire et le titre de la fenêtre n'était pas explicite. Par contre ce même programme dans une fenêtre FLTK occupe seulement 1.9Mo de mémoire. Développé en trois jours, c'est une réécriture plus légère de SPEECH_POP3 https://c.masta.fr/audio/speechAPI/speechPOP3.html qui: - tournait avec l'API Win32 et GTK+ sous Linux - permettait de gérer plusieurs boites - mais fonctionnait sur le port 110 cad non sécurisé (le mot de passe transite en clair par le réseau). INSTALLATION DES HEADERS DE DÉVELOPPEMENT ========================== $ sudo apt install libftlk1.3 libfltk1.3-dev Il est nécessaire de prendre 29,3 Mo dans les archives. Après cette opération, 119 Mo d'espace disque supplémentaires seront utilisés. Les NOUVEAUX paquets suivants seront installés : build-essential dpkg-dev fakeroot fltk1.3-doc fluid g++ g++-13 g++-13-x86-64-linux-gnu g++-x86-64-linux-gnu libalgorithm-diff-perl libalgorithm-diff-xs-perl libalgorithm-merge-perl libfakeroot lto-disabled-list make libfltk-cairo1.3t64 libfltk-forms1.3t64 libfltk-gl1.3t64 libfltk1.3-dev libstdc++-13-dev COMPILATION ======================================================== $ cd dossier $ g++ -Wall -s emails.cpp -o emails -lfltk -lX11 -lpthread EXEMPLE PROTOCOLE POP3 en SSL port 995 ============================= $ openssl s_client -quiet -crlf -connect pop.ionos.fr:995 depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root G2 verify return:1 depth=1 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = GeoTrust TLS RSA CA G1 verify return:1 depth=0 C = FR, ST = Grand Est, L = Sarreguemines, O = IONOS SARL, CN = smtp.ionos.fr verify return:1 +OK POP server ready H mieue111 1MM03T-1sYe5L1y5Z-00IT3l USER joe55@masta.fr +OK password required for user "joe55@masta.fr" PASS xxxxxxxxx +OK mailbox "joe55@masta.fr" has 106 messages (8281006 octets) H mieue111 STAT +OK 106 8281006 LIST +OK 1 1255411 ← c'est l'email le plus ancien en bas de liste 2 654545 3 44541 ... RETR 1 Return-Path: <votre-conseiller@relation-client-edf.fr> Authentication-Results: kundenserver.de; dkim=pass header.i=votre-conseiller@relation-client-edf.fr Received: from omp.relation-client-edf.fr ([140.86.230.217]) by mx.kundenserver.de (mxeue103 [217.72.192.67]) with ESMTPS (Nemesis) id 1McJoo-1r6GOB1gaq-00kRoh for <joe55@masta.fr>; Mon, 19 Feb 2024 12:29:27 +0100 Received: by mta01-am3.am3.responsys.com id hqctpe35r74j for <joe55@masta.fr>; Mon, 19 Feb 2024 11:29:27 +0000 (envelope-from <votre-conseiller@relation-client-edf.fr>) MIME-Version: 1.0 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Date: Mon, 19 Feb 2024 11:29:27 +0000 To: joe55@masta.fr From: =?UTF-8?B?RURG?= <votre-conseiller@relation-client-edf.fr> Reply-To: =?UTF-8?B?RURG?= <ne_pas_repondre@relation-client-edf.fr> Subject: Votre conso du mois dernier <=21DOCTYPE html> <html xmlns=3D=22http://www=2Ew3=2Eorg/1999/xhtml=22 xmlns:o=3D=22urn:schem= as-microsoft-com:office:office=22 xmlns:v=3D=22urn:schemas-microsoft-com:vm= l=22> <head> <title>EDF</title> ... TOP 1 3 ← ne donne que les en-têtes SMTP et les 3 premières ligne du contenu HTML TOP 1 0 ← ne donne que les en-têtes SMTP QUIT +OK POP server signing off à améliorer: il fait une connexion openssl pour chaque message, 1 seule serait mieux */ #include <FL/Fl.H> #include <FL/Fl_Window.H> #include <FL/Fl_Text_Display.H> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <iconv.h> #include <time.h> Fl_Text_Display* disp; Fl_Text_Buffer* buf; /*============================================================================== Ajoute une ligne à l'interface ==============================================================================*/ void addmsg(const char* msg){ buf->append(msg); disp->insert_position(buf->length()); disp->show_insert_position(); Fl::check(); // force refresh window } /*============================================================================== Convertit une chaîne iso-8859-1 ou windows-1252 ou autre vers UTF-8 ==============================================================================*/ char* toUTF8(char* in, size_t len, char* encoding) { //iconv_t iconvDesc = iconv_open("UTF-8//TRANSLIT//IGNORE", "ISO−8859-1"); iconv_t iconvDesc = iconv_open("UTF-8//TRANSLIT//IGNORE", encoding); if (!len){ addmsg("erreur toUTF8: chaine entrée est vide\n"); return NULL;} size_t utf8len = 2*len; char* utf8 = (char*)calloc(utf8len, sizeof(char)); if (!utf8) {addmsg("erreur toUTF8: échec de Calloc()\n");return NULL;} char* utf8start = utf8; iconv(iconvDesc, &in, &len, &utf8, &utf8len); iconv_close(iconvDesc); return utf8start; } /*============================================================================== Base64 decode. Retourne un nouveau string qu'il faudra libérer ==============================================================================*/ char* decoder_base64(char *input, int size) { static const char cd64[]="|$$$}rstuvwxyz{$$$$$$$>?@ABCDEFGHIJKLMNOPQRSTUVW$$$$$$XYZ[\\]^_`abcdefghijklmnopq"; unsigned char in[4], out[4], v; int i, len; int p1 = 0, p2 = 0;//, size=strlen(input)+1; char output[size*3/4 + 1]; while (p1 < size){ for (len=0, i=0; i<4 && p1<size; i++){ v = 0; while (p1<size && v == 0){ v = (unsigned char) input[p1++]; v = (unsigned char) ((v < 43 || v > 122) ? 0 : cd64[v - 43]); if (v) v = (unsigned char) ((v == '$') ? 0 : v - 61); } if (p1 < size){ len++; if (v) in[i] = (unsigned char) (v - 1); } else in[i] = 0; } if (len){ out[0] = (unsigned char) (in[0] << 2 | in[1] >> 4); out[1] = (unsigned char) (in[1] << 4 | in[2] >> 2); out[2] = (unsigned char) (((in[2] << 6) & 0xc0) | in[3]); for (i=0; i<len-1; i++) output[p2++] = out[i]; } } output[p2] = 0; return strdup(output); } /*============================================================================== Transforme un sujet "=?UTF-8?Q?Commande_3041427305705952_:_a_pass=C3=A9_la_douane?=\r\n" en "Commande 3041427305705952 : a_passé la douane" ou en iso-8859-1 "=?iso-8859-1?Q?Vous_avez_re=E7u_30_nouvelles_offres?=\r\n" en "Vous avez reçu 30 nouvelles offres" ou un From en Base64 "=?UTF-8?B?RURG?= <votre-conseiller@relation-client-edf.fr>\r\n" en "EDF <votre-conseiller@relation-client-edf.fr>" ou une partie en B64 "Votre envoi est =?utf-8?b?YXJyaXbDqQ==?= en France\r\n" en "Votre envoi est arrivé en France" "=?windows-1252?Q?Votre_mot_de_passe_Apec.fr_expire_bient=F4t?=" en "Votre mot de passe Apec.fr expire bientôt" ==============================================================================*/ char* decode(const char* s) { //printf("decode(%s)\n", s); char* suj = (char*)malloc(512); unsigned int b=0; unsigned int n=0; char encoding[16]={0}; while(s[n]!='\r' && s[n]!='\n'){ //printf("|%c| ", s[n]); if(s[n]=='=' && s[n+1]=='?'){ n+=2; // survole =? // va au ? suivant en récupèrant l'encoding ex. UTF-8 ou iso-8858-1 unsigned int k=0; while(s[n]!='?'){encoding[k++]=s[n++];} encoding[k]=0; //printf("encoding:(%s)\n", encoding); // sort la lettre: B ou Q char lettre=s[n+1]; n+=3; // survole ?Q? ou ?b? if(lettre=='Q' || lettre=='q'){ // Quoted printable while(1){ if(s[n]=='?' && s[n+1]=='=') break; else if(s[n]=='='){ // transforme =C3 en é char c[3]; c[0]=s[n+1]; c[1]=s[n+2]; c[2]=0; //printf("[%c%s]", s[n],c); char *end; long l = strtol(c,&end,16); sprintf(c,"%c", (int)l); suj[b++] = c[0]; n+=2; } else if(s[n]=='_') suj[b++]=' '; else suj[b++]=s[n]; n++; } suj[b]=0; n+=2; // survole ?= final } // Votre envoi est =?utf-8?b?YXJyaXbDqQ==?= en France else if(lettre=='B' || lettre=='b'){ // Base64 encoded char encoded[256]; unsigned int k=0; while(1){ if(s[n]=='?' && s[n+1]=='=') break; encoded[k++]=s[n++]; } encoded[k]=0; char* d = decoder_base64(encoded, k+1); //printf(" décodage Base 64 (%s)→[%s]\n", encoded, d); suj[b]=0; strcat(suj,d); b+=strlen(d); free(d); n+=2; // survole ?= final } } else suj[b++]=s[n++]; } suj[b]=0; // si encoding existe et n'est pas utf-8 convertit en utf-8 pour afficher correctement les accents dans la console/GUI if(encoding[0] && strcmp(encoding,"UTF-8")){ char* utf = toUTF8(suj, b, encoding); //printf("[%s] → utf8:[%s]\n", suj, utf); strcpy(suj,utf); free(utf); } //printf("decodé(%s)\n", suj); return suj; } /*============================================================================== Se connecte en POP3/TLS et télécharge les en-têtes SMTP du message n°15 vers le fichier local "msg15" pour en extraire FROM, SUBJECT. Et supprime ce fichier local. ==============================================================================*/ void getmsg(char* num) { char fic[8]; FILE* fp; char buf[512]; char from[256]; char sujet[512]; char date[39]; // "Tue, 24 Sep 2024 11:54:36 +0200 (CEST)" 38+1 char champstrouves=0; sprintf(fic,"msg%s",num); sprintf(buf,"openssl s_client -verify_quiet -quiet -connect pop.ionos.fr:995 >%s",fic); fp = popen(buf, "w"); // ATTENTION Ce n'est pas une bonne idée de stocker en clair dans le code les identifiant/passe de votre boite mail. // OK pendant le développement, puis cryptez/obfusquez ces données. //fwrite("USER joe@machin.fr\r\n",20,1,fp); //fwrite("PASS xxxxxxxx\r\n",15,1,fp); CONNEXION_POP3TLS int n = sprintf(buf, "TOP %s 0\r\n", num); // récupère uniquement les en-têtes SMTP sans le contenu fwrite(buf,n,1,fp); fwrite("QUIT\r\n",6,1,fp); pclose(fp); // Ceci fait, parse le fichier tmp pour extraire FROM et SUBJECT fp = fopen(fic, "r"); if(fp == NULL) return; while(fgets(buf, 511, fp) != NULL) { // Date: Wed, 10 Jul 2024 10:31:02 +0000 // Date: Tue, 24 Sep 2024 20:52:52 +0200 (CEST) ← envoyé à 20h52 en septembre: l'heure est bonne // Date: Tue, 24 Sep 2024 11:14:47 -0700 (PDT) ← vient de Chine ici 20h14. Heure d'été du Pacifique // Date: Tue, 24 Sep 2024 11:54:36 -0700 (PDT) ← vient de Chine ici 20h54 11+7=18 +2=20 // donc prendre date et heure sans le -7 ou +2 donne l'heure LOCALE de l'envoi de l'email // UTC : Universal Time Coordinated // PDT : Pacific Daylight Time (PDT) est 7 heures après Coordinated Universal Time (UTC) cad UTC+07 // CEST: Central European Summer Time est 9:00 heures plus tard que Pacific Daylight Time cad UTC+09 // CET : Heure d'Europe centrale UTC+01 if(buf[0]=='D' && buf[1]=='a' && buf[2]=='t' && buf[3]=='e' && buf[4]==':'){ n++; unsigned int k=0; while(buf[n]!='\r'){date[k++]=buf[n++];} // prend toute la valeur du champ //unsigned int n=11; // survole jusqu'à la virgule //unsigned int nbesp=0;// prend le jour mois an //while(nbesp!=4){ //if(buf[n]==' ') nbesp++; //date[k++]=buf[n++]; //} date[k]=0; //printf("DATE:[%s]\n", date); if(++champstrouves==3) break; // pas besoin de parser la suite des en-têtes SMTP } /* Subject: Votre conso du mois dernier ou Subject: La modification de votre annonce "Sacs20cmX5m pour scelleuse sous vide" est en ligne ou Subject: =?UTF-8?Q?Commande_3041427305705952_:_a_pass=C3=A9_la_douane?= ou Subject: Votre envoi est =?utf-8?b?YXJyaXbDqQ==?= en France ou Subject: =?UTF-8?Q?Documents_de_retour_li=C3=A9s_=C3=A0_?= =?UTF-8?Q?votre_commande_n=C2=B0A52499278?= → Documents de retour liés à votre commande n°A52499278 ou sur 2 lignes: Subject: =?UTF-8?q?Votre_colis_pour_la_commande_Poncho_western_spaghetti_NEUF_a_bi?= =?UTF-8?q?en_=C3=A9t=C3=A9_re=C3=A7u?= → Votre colis pour la commande Poncho western spaghetti a bien été reçu ou sur 3 lignes: Subject: =?utf-8?Q?Talent_in_Tech_-_jeudi_3_octobre_-_La_rencontre_des_m=C3=A9?= =?utf-8?Q?tiers_du_num=C3=A9rique_=28job_dating=2C_conf=C3=A9rences=2C_fo?= =?utf-8?Q?rmation=2E=2E=2E=29?= ou Subject: =?iso-8859-1?Q?Vous_avez_re=E7u_30_nouvelles_offres?= Donc si le sujet contient =? le parcourir jusqu'au prochain ? pour prendre l'encoding et voir si la lettre est B (Base64) ou Q (Quoted printable) afin de parcourir jusqu'à ?= pour récupérer le string et transformer "d=E9pos=E9s" en "déposés". Puis voir si la ligne suivante commence par un espace et rebelote. Pour faire un code plus joli il faudrait charger l'intégralité du message dans un string et le parser. Mais plus de mémoire donc ça reste comme ça avec fgets() */ else if(buf[0]=='S' && buf[1]=='u' && buf[2]=='b' && buf[3]=='j' && buf[4]=='e'){ char* suj = decode(buf+9);//printf("SUJET ligne 1:[%s]\n", suj); strcpy(sujet,suj); free(suj); //printf("SUJET1:[%s]\n", sujet); fgets(buf, 511, fp);// si la ligne suivante commence par un espace c'est la 2e ligne sujet à décoder if(buf[0]==' '){ suj = decode(buf+1);//printf("SUJET ligne 2:[%s]\n", suj); strcat(sujet,suj); free(suj); fgets(buf, 511, fp);// si la ligne suivante commence par un espace c'est la 3e ligne sujet à décoder if(buf[0]==' '){ suj = decode(buf+1);//printf("SUJET ligne 3:[%s]\n", suj); strcat(sujet,suj); free(suj); } // sinon le sujet est en fait sur 2 lignes donc regarde si c'est le from else if(buf[0]=='F' && buf[1]=='r' && buf[2]=='o' && buf[3]=='m' && buf[4]==':' && buf[5]==' '){ char* fro = decode(buf+6); strcpy(from,fro); free(fro); } } // sinon le sujet est en fait sur une seule ligne donc regarde si c'est le from else if(buf[0]=='F' && buf[1]=='r' && buf[2]=='o' && buf[3]=='m' && buf[4]==':' && buf[5]==' '){ char* fro = decode(buf+6); strcpy(from,fro); free(fro); } if(++champstrouves==3) break; } /*From: AliExpress <transaction@notice.aliexpress.com> From: leboncoin <no.reply@leboncoin.fr> From: "Oscaro.com" <noreply@oscaro.com> From: =?UTF-8?B?RURG?= <votre-conseiller@relation-client-edf.fr> (cad "EDF" encodé en base64) From: apps.masta.fr/commentaires <b.mas@masta.fr>*/ else if(buf[0]=='F' && buf[1]=='r' && buf[2]=='o' && buf[3]=='m' && buf[4]==':' && buf[5]==' '){ //printf("FROM trouvé\n"); char* fro = decode(buf+6); strcpy(from,fro); free(fro); // regarde la ligne suivante car Leboncoin envoie ça: //From: // Solange RIVIERE via leboncoin <3jq6lllt7r07v56n@messagerie.leboncoin.fr> fgets(buf, 511, fp);// si la ligne suivante commence par un espace c'est la 3e ligne sujet à décoder if(buf[0]==' ' || buf[0]=='\t'){ fro = decode(buf+1);//printf("SUJET ligne 3:[%s]\n", suj); strcat(from,fro); free(fro); } /* unsigned int b=0; unsigned int n=6; // à partir du 6e octet, après "From: " if(buf[6]=='=' && buf[7]=='?'){ //Voir à l'usage si le from peut être à moitié encodé genre From: Ali=?UTF-8?B?RURG?=Express char* fro = decode(buf+5); strcpy(from,fro); free(fro); } else{ while(buf[n]!='\r' && buf[n]!='\n'){from[b++]=buf[n++];} from[b]=0; } //printf("FROM:[%s]%d\n", from, b); if(++champstrouves==3) break;*/ } } fclose(fp); remove(fic); char msg[965]; // from fait 37 octets. Je veux le from puis des espaces jusqu'à 60 octets char espaces[66]; int lenfrom = strlen(from); if(lenfrom > 65){ espaces[0]=' '; espaces[1]=0; } else{ int nbespaces = 65 - lenfrom; for(n=0; n<nbespaces; n++){espaces[n]='-';} espaces[n]=0; } //printf("\t%s|%s|%s|%s\n", date, from, espaces, sujet); sprintf(msg, " %s|%s|%s|%s\n", date, from, espaces, sujet); addmsg(msg); } /*==============================================================================*/ char* file_get_contents(const char* file) { FILE* fp = fopen(file, "rb"); if (fp==NULL) return NULL;//printf("file_get_contents(%s) : Fichier introuvable\n", file);} fseek(fp, 0, SEEK_END); long n = ftell(fp); rewind(fp); char* c = (char*) malloc(n+1); if (c==NULL)return NULL;//printf("malloc a échoué sur file_get_contents(%s)\n", file);} fread(c, 1, n, fp); c[n]=0; fclose(fp); return c; } /*==============================================================================*/ void file_put_contents(const char* fic, char*contenu, int len) { FILE* fp = fopen (fic, "w"); fwrite (contenu, 1, len, fp); fclose (fp); } /*================================================================================ Synthèse vocale non bloquante dans un thread ================================================================================*/ void* thread_synthese_vocale(void* arg) { char cmd[512]; int* n = (int*)arg; sprintf(cmd, "espeak -p75 -v mb-fr4 \"Vous avez. %d... nouveaux messages\" | mbrola /usr/share/mbrola/fr4/fr4 - -", *n); system(cmd); pthread_exit(EXIT_SUCCESS); // Arrêt propre du thread } /*==============================================================================*/ void* thread_reseau(void *arg) { while(1){ //printf("Connexion POP:995 → "); // openssl ne permet pas de lire et écrire donc dirige la sortie vers un fichier tmp FILE* fp = popen("openssl s_client -verify_quiet -quiet -connect pop.ionos.fr:995 >tmp", "w"); // ATTENTION Ce n'est pas une bonne idée de stocker en clair dans le code les identifiant/passe de votre boite mail. // OK pendant le développement, puis cryptez/obfusquez ces données. fwrite("USER joe@machin.fr\r\n",20,1,fp); fwrite("PASS xxxxxxxx\r\n",15,1,fp); fwrite("QUIT\r\n",6,1,fp); pclose(fp); /* Ensuite lit le fichier tmp. Exemple de sortie: +OK POP server ready H mieue009 1MG8wm-1skuzV48gG-00HNyS +OK password required for user "joe55@masta.fr" +OK mailbox "joe55@masta.fr" has 110 messages (8682769 octets) H mieue009 +OK POP server signing off Donc ne regarde que la 3e ligne à partir du 33e octet jusqu'à l'espace pour récupérer 110 (on aurait pu envoyer une commande STAT → +OK 110 8682769 mais non car ça fait une commande en moins pour le serveur) */ fp = fopen("tmp", "r"); if(fp != NULL){ char ligne=0; char buf[512]; while(fgets(buf, sizeof(buf)-1, fp) != NULL) { if(++ligne!=3) continue; // à la 3e ligne char nb[6]; unsigned short n=33; unsigned int b=0; // à partir du 33e octet jusqu'à l'espace pour récupérer 110 while(buf[n]!=' '){nb[b++]=buf[n++];} nb[b]=0; int newnb = atoi(nb); //printf("il y a %s messages sur le serveur\n", nb); // récupère l'ancien nombre contenu dans le fichier char* s = file_get_contents("nbmessages"); int oldnb = (s!=NULL) ? atoi(s) : 0; free(s); // pas de fuite après malloc()! //printf("vous aviez %d messages\n", oldnb); if(oldnb < newnb){ int diff = newnb - oldnb; const char* lex = "X "; const char* les = "s "; time_t ti = time(NULL); struct tm tm = *localtime(&ti); char msg[59]; sprintf(msg, "%d NOUVEAU%smessage%sle %02d/%02d/%d à %02dh%02d\n", diff, (diff>1)?lex:" ", (diff>1)?les:" ", tm.tm_mday,tm.tm_mon+1,tm.tm_year+1900, tm.tm_hour,tm.tm_min);//,tm.tm_sec); addmsg(msg); // stocke en local le nouveau nombre de messages file_put_contents("nbmessages", nb, b); // Synthèse vocale non bloquante: "Vous avez. diff... nouveaux messages" pthread_t t; pthread_create(&t, NULL, thread_synthese_vocale, &diff); // Récupération des en-têtes SMTP pour avoir FROM et SUBJECT de chaque nouveau message for(b=newnb; (int)b>oldnb; b--){ char num[5]; sprintf(num,"%d",b); //itoa() est inconnu dans stdlib //printf("get msg %s\n", num); getmsg(num); } } break; // pas besoin de parser la 4e ligne ni au-delà } fclose(fp); remove("tmp"); } //printf("attendre 60s...\n"); sleep(60); } } /*============================================================================== Point d'entrée ================================================================================*/ int main(int argc, char** argv) { Fl_Window *win = new Fl_Window(1300,480, "emails"); win->position((Fl::w() - win->w())/2, (Fl::h() - win->h())/2); buf = new Fl_Text_Buffer(); disp = new Fl_Text_Display(0,0, 1300,480); disp->color(FL_BLACK); disp->textcolor(FL_WHITE); disp->buffer(buf); win->resizable(*disp); win->show(); pthread_t t; pthread_create(&t, NULL, thread_reseau, NULL); return(Fl::run()); }