├── .gitignore ├── Google_Email_Example.dpr ├── Google_Email_Example.dproj ├── Google_Email_Example.skincfg ├── LICENSE ├── README.md ├── U_DCS_OAuth2.pas ├── U_emailExample.dfm └── U_emailExample.pas /.gitignore: -------------------------------------------------------------------------------- 1 | __history/ 2 | __recovery/ 3 | Win32/ 4 | Win64/ 5 | Win64x/ 6 | 7 | *.~* 8 | *.bak 9 | *.dsk 10 | *.dsv 11 | *.hpp 12 | *.identcache 13 | *.local 14 | *.res 15 | *.stat 16 | *.tds 17 | *.tvsconfig 18 | -------------------------------------------------------------------------------- /Google_Email_Example.dpr: -------------------------------------------------------------------------------- 1 | program Google_Email_Example; 2 | 3 | uses 4 | Vcl.Forms, 5 | U_emailExample in 'U_emailExample.pas' {FRM_sendMail}, 6 | U_DCS_OAuth2 in 'U_DCS_OAuth2.pas'; 7 | 8 | {$R *.res} 9 | 10 | begin 11 | Application.Initialize; 12 | Application.MainFormOnTaskbar := True; 13 | Application.CreateForm(TFRM_sendMail, FRM_sendMail); 14 | Application.Run; 15 | end. 16 | -------------------------------------------------------------------------------- /Google_Email_Example.dproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | {D2CF27A5-F54E-4683-B706-7CF724622F5F} 4 | 19.1 5 | VCL 6 | True 7 | Debug 8 | Win32 9 | 1 10 | Application 11 | Google_Email_Example.dpr 12 | 13 | 14 | true 15 | 16 | 17 | true 18 | Base 19 | true 20 | 21 | 22 | true 23 | Base 24 | true 25 | 26 | 27 | true 28 | Base 29 | true 30 | 31 | 32 | true 33 | Cfg_1 34 | true 35 | true 36 | 37 | 38 | true 39 | Base 40 | true 41 | 42 | 43 | true 44 | Cfg_2 45 | true 46 | true 47 | 48 | 49 | .\$(Platform)\$(Config) 50 | .\$(Platform)\$(Config) 51 | false 52 | false 53 | false 54 | false 55 | false 56 | System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace) 57 | $(BDS)\bin\delphi_PROJECTICON.ico 58 | $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png 59 | $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png 60 | Google_Email_Example 61 | 62 | 63 | DBXSqliteDriver;RESTComponents;fmxase;DBXInterBaseDriver;dxSpreadSheetConditionalFormattingDialogsRS27;vclactnband;vclFireDAC;dxSpreadSheetReportDesignerRS27;dxSpreadSheetRS27;bindcompvclsmp;tethering;svnui;FireDACADSDriver;cxLibraryRS27;dxADOServerModeRS27;vcltouch;vcldb;bindcompfmx;svn;dxPScxExtCommonRS27;inetdb;cxTreeListRS27;fmx;FireDACIBDriver;fmxdae;vcledge;dbexpress;IndyCore;vclx;dsnap;FireDACCommon;DCS_Components;RESTBackendComponents;dclZipForged27;VCLRESTComponents;soapserver;dxPScxTLLnkRS27;vclie;bindengine;DBXMySQLDriver;CloudService;dxPSCoreRS27;FireDACMySQLDriver;cxExportRS27;FireDACCommonODBC;FireDACCommonDriver;inet;IndyIPCommon;bindcompdbx;vcl;IndyIPServer;dxPScxPCProdRS27;IndySystem;dxHttpIndyRequestRS27;dsnapcon;dxComnRS27;FireDACMSAccDriver;fmxFireDAC;dxmdsRS27;vclimg;FireDAC;dxCoreRS27;dxPSdxSpreadSheetLnkRS27;FireDACSqliteDriver;FireDACPgDriver;DCS_dxModernRS27;soaprtl;DbxCommonDriver;dxCloudServiceLibraryRS27;xmlrtl;soapmidas;fmxobj;vclwinx;dxPScxCommonRS27;rtl;VpevclXe2;DbxClientDriver;CustomIPTransport;vcldsnap;dxSkinsCoreRS27;dxPScxGridLnkRS27;vclZipForged27;bindcomp;appanalytics;dxSpreadSheetCoreRS27;dxGDIPlusRS27;IndyIPClient;dxFireDACServerModeRS27;bindcompvcl;dxServerModeRS27;dxPSLnksRS27;dbxcds;VclSmp;adortl;dxDBXServerModeRS27;cxGridRS27;dsnapxml;dbrtl;IndyProtocols;inetdbxpress;dxPSdxLCLnkRS27;dxSpreadSheetCoreConditionalFormattingDialogsRS27;$(DCC_UsePackage) 64 | Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) 65 | Debug 66 | true 67 | CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= 68 | 1033 69 | $(BDS)\bin\default_app.manifest 70 | 71 | 72 | DBXSqliteDriver;RESTComponents;fmxase;DBXInterBaseDriver;dxSpreadSheetConditionalFormattingDialogsRS27;vclactnband;vclFireDAC;dxSpreadSheetReportDesignerRS27;dxSpreadSheetRS27;bindcompvclsmp;tethering;FireDACADSDriver;cxLibraryRS27;dxADOServerModeRS27;vcltouch;vcldb;bindcompfmx;dxPScxExtCommonRS27;inetdb;cxTreeListRS27;fmx;FireDACIBDriver;fmxdae;vcledge;dbexpress;IndyCore;vclx;dsnap;FireDACCommon;RESTBackendComponents;VCLRESTComponents;soapserver;dxPScxTLLnkRS27;vclie;bindengine;DBXMySQLDriver;CloudService;dxPSCoreRS27;FireDACMySQLDriver;cxExportRS27;FireDACCommonODBC;FireDACCommonDriver;inet;IndyIPCommon;bindcompdbx;vcl;IndyIPServer;dxPScxPCProdRS27;IndySystem;dxHttpIndyRequestRS27;dsnapcon;dxComnRS27;FireDACMSAccDriver;fmxFireDAC;dxmdsRS27;vclimg;FireDAC;dxCoreRS27;dxPSdxSpreadSheetLnkRS27;FireDACSqliteDriver;FireDACPgDriver;soaprtl;DbxCommonDriver;dxCloudServiceLibraryRS27;xmlrtl;soapmidas;fmxobj;vclwinx;dxPScxCommonRS27;rtl;DbxClientDriver;CustomIPTransport;vcldsnap;dxSkinsCoreRS27;dxPScxGridLnkRS27;vclZipForged27;bindcomp;appanalytics;dxSpreadSheetCoreRS27;dxGDIPlusRS27;IndyIPClient;dxFireDACServerModeRS27;bindcompvcl;dxServerModeRS27;dxPSLnksRS27;dbxcds;VclSmp;adortl;dxDBXServerModeRS27;cxGridRS27;dsnapxml;dbrtl;IndyProtocols;inetdbxpress;dxPSdxLCLnkRS27;dxSpreadSheetCoreConditionalFormattingDialogsRS27;$(DCC_UsePackage) 73 | 74 | 75 | DEBUG;$(DCC_Define) 76 | true 77 | false 78 | true 79 | true 80 | true 81 | 82 | 83 | false 84 | true 85 | PerMonitorV2 86 | 87 | 88 | false 89 | RELEASE;$(DCC_Define) 90 | 0 91 | 0 92 | 93 | 94 | true 95 | PerMonitorV2 96 | 97 | 98 | 99 | MainSource 100 | 101 | 102 |
FRM_sendMail
103 | dfm 104 |
105 | 106 | 107 | Cfg_2 108 | Base 109 | 110 | 111 | Base 112 | 113 | 114 | Cfg_1 115 | Base 116 | 117 |
118 | 119 | Delphi.Personality.12 120 | Application 121 | 122 | 123 | 124 | Google_Email_Example.dpr 125 | 126 | 127 | 128 | 129 | 130 | Google_Email_Example.exe 131 | true 132 | 133 | 134 | 135 | 136 | 1 137 | 138 | 139 | Contents\MacOS 140 | 1 141 | 142 | 143 | 0 144 | 145 | 146 | 147 | 148 | classes 149 | 1 150 | 151 | 152 | classes 153 | 1 154 | 155 | 156 | 157 | 158 | res\xml 159 | 1 160 | 161 | 162 | res\xml 163 | 1 164 | 165 | 166 | 167 | 168 | library\lib\armeabi-v7a 169 | 1 170 | 171 | 172 | 173 | 174 | library\lib\armeabi 175 | 1 176 | 177 | 178 | library\lib\armeabi 179 | 1 180 | 181 | 182 | 183 | 184 | library\lib\armeabi-v7a 185 | 1 186 | 187 | 188 | 189 | 190 | library\lib\mips 191 | 1 192 | 193 | 194 | library\lib\mips 195 | 1 196 | 197 | 198 | 199 | 200 | library\lib\armeabi-v7a 201 | 1 202 | 203 | 204 | library\lib\arm64-v8a 205 | 1 206 | 207 | 208 | 209 | 210 | library\lib\armeabi-v7a 211 | 1 212 | 213 | 214 | 215 | 216 | res\drawable 217 | 1 218 | 219 | 220 | res\drawable 221 | 1 222 | 223 | 224 | 225 | 226 | res\values 227 | 1 228 | 229 | 230 | res\values 231 | 1 232 | 233 | 234 | 235 | 236 | res\values-v21 237 | 1 238 | 239 | 240 | res\values-v21 241 | 1 242 | 243 | 244 | 245 | 246 | res\values 247 | 1 248 | 249 | 250 | res\values 251 | 1 252 | 253 | 254 | 255 | 256 | res\drawable 257 | 1 258 | 259 | 260 | res\drawable 261 | 1 262 | 263 | 264 | 265 | 266 | res\drawable-xxhdpi 267 | 1 268 | 269 | 270 | res\drawable-xxhdpi 271 | 1 272 | 273 | 274 | 275 | 276 | res\drawable-ldpi 277 | 1 278 | 279 | 280 | res\drawable-ldpi 281 | 1 282 | 283 | 284 | 285 | 286 | res\drawable-mdpi 287 | 1 288 | 289 | 290 | res\drawable-mdpi 291 | 1 292 | 293 | 294 | 295 | 296 | res\drawable-hdpi 297 | 1 298 | 299 | 300 | res\drawable-hdpi 301 | 1 302 | 303 | 304 | 305 | 306 | res\drawable-xhdpi 307 | 1 308 | 309 | 310 | res\drawable-xhdpi 311 | 1 312 | 313 | 314 | 315 | 316 | res\drawable-mdpi 317 | 1 318 | 319 | 320 | res\drawable-mdpi 321 | 1 322 | 323 | 324 | 325 | 326 | res\drawable-hdpi 327 | 1 328 | 329 | 330 | res\drawable-hdpi 331 | 1 332 | 333 | 334 | 335 | 336 | res\drawable-xhdpi 337 | 1 338 | 339 | 340 | res\drawable-xhdpi 341 | 1 342 | 343 | 344 | 345 | 346 | res\drawable-xxhdpi 347 | 1 348 | 349 | 350 | res\drawable-xxhdpi 351 | 1 352 | 353 | 354 | 355 | 356 | res\drawable-xxxhdpi 357 | 1 358 | 359 | 360 | res\drawable-xxxhdpi 361 | 1 362 | 363 | 364 | 365 | 366 | res\drawable-small 367 | 1 368 | 369 | 370 | res\drawable-small 371 | 1 372 | 373 | 374 | 375 | 376 | res\drawable-normal 377 | 1 378 | 379 | 380 | res\drawable-normal 381 | 1 382 | 383 | 384 | 385 | 386 | res\drawable-large 387 | 1 388 | 389 | 390 | res\drawable-large 391 | 1 392 | 393 | 394 | 395 | 396 | res\drawable-xlarge 397 | 1 398 | 399 | 400 | res\drawable-xlarge 401 | 1 402 | 403 | 404 | 405 | 406 | res\values 407 | 1 408 | 409 | 410 | res\values 411 | 1 412 | 413 | 414 | 415 | 416 | 1 417 | 418 | 419 | Contents\MacOS 420 | 1 421 | 422 | 423 | 0 424 | 425 | 426 | 427 | 428 | Contents\MacOS 429 | 1 430 | .framework 431 | 432 | 433 | Contents\MacOS 434 | 1 435 | .framework 436 | 437 | 438 | 0 439 | 440 | 441 | 442 | 443 | 1 444 | .dylib 445 | 446 | 447 | 1 448 | .dylib 449 | 450 | 451 | 1 452 | .dylib 453 | 454 | 455 | Contents\MacOS 456 | 1 457 | .dylib 458 | 459 | 460 | Contents\MacOS 461 | 1 462 | .dylib 463 | 464 | 465 | 0 466 | .dll;.bpl 467 | 468 | 469 | 470 | 471 | 1 472 | .dylib 473 | 474 | 475 | 1 476 | .dylib 477 | 478 | 479 | 1 480 | .dylib 481 | 482 | 483 | Contents\MacOS 484 | 1 485 | .dylib 486 | 487 | 488 | Contents\MacOS 489 | 1 490 | .dylib 491 | 492 | 493 | 0 494 | .bpl 495 | 496 | 497 | 498 | 499 | 0 500 | 501 | 502 | 0 503 | 504 | 505 | 0 506 | 507 | 508 | 0 509 | 510 | 511 | 0 512 | 513 | 514 | Contents\Resources\StartUp\ 515 | 0 516 | 517 | 518 | Contents\Resources\StartUp\ 519 | 0 520 | 521 | 522 | 0 523 | 524 | 525 | 526 | 527 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 528 | 1 529 | 530 | 531 | 532 | 533 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 534 | 1 535 | 536 | 537 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 538 | 1 539 | 540 | 541 | 542 | 543 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 544 | 1 545 | 546 | 547 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 548 | 1 549 | 550 | 551 | 552 | 553 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 554 | 1 555 | 556 | 557 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 558 | 1 559 | 560 | 561 | 562 | 563 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 564 | 1 565 | 566 | 567 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 568 | 1 569 | 570 | 571 | 572 | 573 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 574 | 1 575 | 576 | 577 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 578 | 1 579 | 580 | 581 | 582 | 583 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 584 | 1 585 | 586 | 587 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 588 | 1 589 | 590 | 591 | 592 | 593 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 594 | 1 595 | 596 | 597 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 598 | 1 599 | 600 | 601 | 602 | 603 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 604 | 1 605 | 606 | 607 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 608 | 1 609 | 610 | 611 | 612 | 613 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 614 | 1 615 | 616 | 617 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 618 | 1 619 | 620 | 621 | 622 | 623 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 624 | 1 625 | 626 | 627 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 628 | 1 629 | 630 | 631 | 632 | 633 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 634 | 1 635 | 636 | 637 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 638 | 1 639 | 640 | 641 | 642 | 643 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 644 | 1 645 | 646 | 647 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 648 | 1 649 | 650 | 651 | 652 | 653 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 654 | 1 655 | 656 | 657 | ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 658 | 1 659 | 660 | 661 | 662 | 663 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 664 | 1 665 | 666 | 667 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 668 | 1 669 | 670 | 671 | 672 | 673 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 674 | 1 675 | 676 | 677 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 678 | 1 679 | 680 | 681 | 682 | 683 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 684 | 1 685 | 686 | 687 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 688 | 1 689 | 690 | 691 | 692 | 693 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 694 | 1 695 | 696 | 697 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 698 | 1 699 | 700 | 701 | 702 | 703 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 704 | 1 705 | 706 | 707 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 708 | 1 709 | 710 | 711 | 712 | 713 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 714 | 1 715 | 716 | 717 | ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 718 | 1 719 | 720 | 721 | 722 | 723 | 1 724 | 725 | 726 | 1 727 | 728 | 729 | 730 | 731 | ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF 732 | 1 733 | 734 | 735 | ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF 736 | 1 737 | 738 | 739 | 740 | 741 | ..\ 742 | 1 743 | 744 | 745 | ..\ 746 | 1 747 | 748 | 749 | 750 | 751 | 1 752 | 753 | 754 | 1 755 | 756 | 757 | 1 758 | 759 | 760 | 761 | 762 | ..\$(PROJECTNAME).launchscreen 763 | 64 764 | 765 | 766 | ..\$(PROJECTNAME).launchscreen 767 | 64 768 | 769 | 770 | 771 | 772 | 1 773 | 774 | 775 | 1 776 | 777 | 778 | 1 779 | 780 | 781 | 782 | 783 | ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF 784 | 1 785 | 786 | 787 | 788 | 789 | ..\ 790 | 1 791 | 792 | 793 | ..\ 794 | 1 795 | 796 | 797 | 798 | 799 | Contents 800 | 1 801 | 802 | 803 | Contents 804 | 1 805 | 806 | 807 | 808 | 809 | Contents\Resources 810 | 1 811 | 812 | 813 | Contents\Resources 814 | 1 815 | 816 | 817 | 818 | 819 | library\lib\armeabi-v7a 820 | 1 821 | 822 | 823 | library\lib\arm64-v8a 824 | 1 825 | 826 | 827 | 1 828 | 829 | 830 | 1 831 | 832 | 833 | 1 834 | 835 | 836 | 1 837 | 838 | 839 | Contents\MacOS 840 | 1 841 | 842 | 843 | Contents\MacOS 844 | 1 845 | 846 | 847 | 0 848 | 849 | 850 | 851 | 852 | library\lib\armeabi-v7a 853 | 1 854 | 855 | 856 | 857 | 858 | 1 859 | 860 | 861 | 1 862 | 863 | 864 | 865 | 866 | Assets 867 | 1 868 | 869 | 870 | Assets 871 | 1 872 | 873 | 874 | 875 | 876 | Assets 877 | 1 878 | 879 | 880 | Assets 881 | 1 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | True 897 | False 898 | 899 | 900 | 12 901 | 902 | 903 | 904 | 905 |
906 | -------------------------------------------------------------------------------- /Google_Email_Example.skincfg: -------------------------------------------------------------------------------- 1 | [ExpressSkins] 2 | Default=0 3 | ShowNotifications=1 4 | Enabled=1 5 | dxSkinBasic=1 6 | dxSkinBlack=1 7 | dxSkinBlue=0 8 | dxSkinBlueprint=0 9 | dxSkinCaramel=0 10 | dxSkinCoffee=0 11 | dxSkinDarkroom=0 12 | dxSkinDarkSide=0 13 | DCS_dxModern=1 14 | dxSkinDevExpressDarkStyle=0 15 | dxSkinDevExpressStyle=0 16 | dxSkinFoggy=0 17 | dxSkinGlassOceans=0 18 | dxSkinHighContrast=0 19 | dxSkiniMaginary=0 20 | dxSkinLilian=0 21 | dxSkinLiquidSky=0 22 | dxSkinLondonLiquidSky=0 23 | dxSkinMcSkin=0 24 | dxSkinMetropolis=0 25 | dxSkinMetropolisDark=0 26 | dxSkinMoneyTwins=0 27 | dxSkinOffice2007Black=0 28 | dxSkinOffice2007Blue=0 29 | dxSkinOffice2007Green=0 30 | dxSkinOffice2007Pink=0 31 | dxSkinOffice2007Silver=0 32 | dxSkinOffice2010Black=0 33 | dxSkinOffice2010Blue=0 34 | dxSkinOffice2010Silver=0 35 | dxSkinOffice2013DarkGray=0 36 | dxSkinOffice2013LightGray=0 37 | dxSkinOffice2013White=0 38 | dxSkinOffice2016Colorful=1 39 | dxSkinOffice2016Dark=1 40 | dxSkinOffice2019Black=1 41 | dxSkinOffice2019Colorful=1 42 | dxSkinOffice2019DarkGray=1 43 | dxSkinOffice2019White=1 44 | dxSkinPumpkin=0 45 | dxSkinSeven=0 46 | dxSkinSevenClassic=0 47 | dxSkinSharp=0 48 | dxSkinSharpPlus=0 49 | dxSkinSilver=0 50 | dxSkinSpringtime=0 51 | dxSkinStardust=0 52 | dxSkinSummer2008=0 53 | dxSkinTheAsphaltWorld=0 54 | dxSkinTheBezier=1 55 | dxSkinsDefaultPainters=0 56 | dxSkinValentine=0 57 | dxSkinVisualStudio2013Blue=1 58 | dxSkinVisualStudio2013Dark=1 59 | dxSkinVisualStudio2013Light=1 60 | dxSkinVS2010=0 61 | dxSkinWhiteprint=0 62 | dxSkinXmas2008Blue=0 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # delphi-google-oauth2 2 | Browser enabled TCustomAuthenticator for Delphi TRestClient 3 | 4 | This unit is inspired on Delphi's OAuth2 code but: 5 | - Allows authorization via externel browser 6 | - Uses PKCE flow for added security 7 | - Allows gereration of new tokens when they expire (using the refresh token) 8 | 9 | # Dependencies 10 | - You will need libeay32.dll and ssleay32.dll in the same folder of your applications's .exe file for the Authenticator to work (becouse PKCE uses SHA256). 11 | - You will need Indy 12 | 13 | # Test authenticator using the demo application (Google_Email_Example) 14 | Open the project and fill in your Application's ClientID and ClientSecret on procedure googleAPI_prepare: 15 | 16 | ```pascal 17 | // Application specific options (created on Google's console) 18 | DCSOAuth2Authenticator.ClientID := 'your ClientID goes here'; // ClientID created on console.developers.google.com 19 | DCSOAuth2Authenticator.ClientSecret := 'your ClientSecret goes here'; // ClientSecret for the application registered on console.developers.google.com 20 | ``` 21 | -------------------------------------------------------------------------------- /U_DCS_OAuth2.pas: -------------------------------------------------------------------------------- 1 | // =========================================================================== 2 | // Copyright 2020 DCS, Lda 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | // ============ IMPORTANT ==================================================== 17 | // This unit is inspired on Delphi's OAuth2 code but: 18 | // - Allows authorization via externel browser 19 | // - Uses PKCE flow for added security 20 | // - Allows gereration of new tokens when they expire (using the refresh token) 21 | // =========================================================================== 22 | 23 | {$HPPEMIT LINKUNIT} 24 | unit U_DCS_OAuth2; 25 | 26 | interface 27 | 28 | uses 29 | System.Classes, 30 | System.SysUtils, 31 | Data.Bind.ObjectScope, 32 | Data.Bind.Components, 33 | REST.Client, 34 | REST.Types, 35 | REST.Consts, 36 | REST.Utils, 37 | REST.BindSource, 38 | IdCustomHTTPServer, 39 | IdHTTPServer, 40 | IdContext; 41 | 42 | {$SCOPEDENUMS ON} 43 | 44 | const 45 | K_invalidAuth = 'invalidAuth'; 46 | 47 | type 48 | TDCSOAuth2ResponseType = (rtCODE, rtTOKEN); // rtCODE Default workflow including the authentication of the client - rtTOKEN Implicit workflow for direct requesting an accesstoken 49 | TDCSOAuth2TokenType = (ttNONE, ttBEARER); 50 | TDCSTokenRequestType = (trtAuthGetTokens, trtRefreshTokens); 51 | 52 | TDCSOAuth2Authenticator = class; 53 | TDCSSubOAuth2AuthBindSource = class; 54 | EOAuth2Exception = class(ERESTException); 55 | 56 | TDCSOAuth2Authenticator = class(TCustomAuthenticator) 57 | private 58 | { Private declarations } 59 | FBindSource: TDCSSubOAuth2AuthBindSource; 60 | FAccessToken: string; 61 | FAccessTokenEndpoint: string; 62 | FAccessTokenExpiry: TDateTime; 63 | FAccessTokenParamName: string; 64 | FAuthCode: string; 65 | FAuthorizationEndpoint: string; 66 | FClientID: string; 67 | FClientSecret: string; 68 | FLocalState: string; 69 | FCodeVerifier: string; 70 | FCodeChallenge: string; 71 | FRedirectionEndpoint: string; 72 | FRefreshToken: string; 73 | FResponseType: TDCSOAuth2ResponseType; 74 | FScope: string; 75 | FTokenType: TDCSOAuth2TokenType; 76 | FLoginHint: string; 77 | 78 | privLS: TIdHTTPServer; // LS: Local server (Used to get the Auth code from the localhost redirect by the service provider) 79 | privLS_port: integer; 80 | privTempAuthCode: string; 81 | 82 | procedure SetAccessTokenEndpoint(const AValue: string); 83 | procedure SetAccessTokenParamName(const AValue: string); 84 | procedure SetAuthCode(const AValue: string); 85 | procedure SetAuthorizationEndpoint(const AValue: string); 86 | procedure SetClientID(const AValue: string); 87 | procedure SetClientSecret(const AValue: string); 88 | procedure SetLocalState(const AValue: string); 89 | procedure SetRedirectionEndpoint(const AValue: string); 90 | procedure SetRefreshToken(const AValue: string); 91 | procedure SetResponseType(const AValue: TDCSOAuth2ResponseType); 92 | procedure SetScope(const AValue: string); 93 | function ResponseTypeIsStored: Boolean; 94 | function TokenTypeIsStored: Boolean; 95 | function AccessTokenParamNameIsStored: Boolean; 96 | procedure ReadAccessTokenExpiryData(AReader: TReader); 97 | procedure SetAccessToken(const AValue: string); 98 | procedure SetAccessTokenExpiry(const AExpiry: TDateTime); 99 | procedure SetTokenType(const AType: TDCSOAuth2TokenType); 100 | procedure WriteAccessTokenExpiryData(AWriter: TWriter); 101 | 102 | procedure LS_start; 103 | procedure LS_stop; 104 | procedure LS_onCommandGet (AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo); 105 | procedure LS_onCommandError(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo; AException: Exception); 106 | function LS_getMsgPage_html(errorStr: string = ''): string; 107 | function LS_getFreePort: integer; 108 | 109 | function generate_randomString(strLength: integer): string; 110 | function encode_SHA256_base64URL(str_toEncode: string): string; 111 | protected 112 | { Protected declarations } 113 | procedure DefineProperties(Filer: TFiler); override; 114 | procedure DoAuthenticate(ARequest: TCustomRESTRequest); override; 115 | function CreateBindSource: TBaseObjectBindSource; override; 116 | public 117 | { Public declarations } 118 | constructor Create(AOwner: TComponent); override; 119 | 120 | procedure Assign(ASource: TDCSOAuth2Authenticator); reintroduce; 121 | procedure ResetToDefaults; override; 122 | 123 | function getLocalRedirectionURL_andSetPort: string; 124 | function AuthorizationRequestURI: string; 125 | 126 | procedure AquireAccessToken_browser; 127 | procedure GetTokens_fromAuthCode; 128 | procedure GetTokens_fromRefreshToken; 129 | procedure GetTokens(requestType: TDCSTokenRequestType); 130 | published 131 | { Published properties } 132 | property AccessToken: string read FAccessToken write SetAccessToken; 133 | property AccessTokenEndpoint: string read FAccessTokenEndpoint write SetAccessTokenEndpoint; 134 | property AccessTokenExpiry: TDateTime read FAccessTokenExpiry write SetAccessTokenExpiry; 135 | property AccessTokenParamName: string read FAccessTokenParamName write SetAccessTokenParamName stored AccessTokenParamNameIsStored; 136 | property AuthCode: string read FAuthCode write SetAuthCode; 137 | property AuthorizationEndpoint: string read FAuthorizationEndpoint write SetAuthorizationEndpoint; 138 | property ClientID: string read FClientID write SetClientID; 139 | property ClientSecret: string read FClientSecret write SetClientSecret; 140 | property LocalState: string read FLocalState write SetLocalState; 141 | property CodeVerifier: string read FCodeVerifier write FCodeVerifier; 142 | property CodeChallenge: string read FCodeChallenge write FCodeChallenge; 143 | property RedirectionEndpoint: string read FRedirectionEndpoint write SetRedirectionEndpoint; 144 | property RefreshToken: string read FRefreshToken write SetRefreshToken; 145 | property ResponseType: TDCSOAuth2ResponseType read FResponseType write SetResponseType stored ResponseTypeIsStored; 146 | property Scope: string read FScope write SetScope; 147 | property TokenType: TDCSOAuth2TokenType read FTokenType write SetTokenType stored TokenTypeIsStored; 148 | property LoginHint: string read FLoginHint write FLoginHint; 149 | property BindSource: TDCSSubOAuth2AuthBindSource read FBindSource; 150 | end; 151 | 152 | // *************************************************************************************** 153 | // LiveBindings bindsource for TDCSOAuth2Authenticator. Publishes subcomponent properties 154 | TDCSSubOAuth2AuthBindSource = class(TRESTAuthenticatorBindSource) 155 | protected 156 | function CreateAdapterT: TRESTAuthenticatorAdapter; override; 157 | end; 158 | 159 | // *********************************************************************** 160 | /// LiveBindings adapter for TOAuth2Authenticator. Create bindable members 161 | TDCSOAuth2AuthAdapter = class(TRESTAuthenticatorAdapter) 162 | protected 163 | procedure AddFields; override; 164 | end; 165 | 166 | 167 | function DCSOAuth2ResponseTypeToString (const AType: TDCSOAuth2ResponseType): string; 168 | function DCSOAuth2ResponseTypeFromString(const ATypeString: string): TDCSOAuth2ResponseType; 169 | 170 | function DCSOAuth2TokenTypeToString (const AType: TDCSOAuth2TokenType): string; 171 | function DCSOAuth2TokenTypeFromString(const ATypeString: string): TDCSOAuth2TokenType; 172 | 173 | var 174 | DefaultOAuth2ResponseType: TDCSOAuth2ResponseType = TDCSOAuth2ResponseType.rtCODE; 175 | DefaultOAuth2TokenType: TDCSOAuth2TokenType = TDCSOAuth2TokenType.ttNONE; 176 | DefaultOAuth2AccessTokenParamName: string = 'access_token'; // do not localize 177 | 178 | 179 | implementation 180 | 181 | uses 182 | System.DateUtils, System.NetEncoding, 183 | Winapi.Windows, Winapi.ShellAPI, Win.ScktComp, 184 | IdHashSHA, IdSSLOpenSSL, IdGlobal; 185 | 186 | 187 | 188 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 189 | { ******* // // ******* } 190 | { ******* // TDCSOAuth2Authenticator // ******* } 191 | { ******* // // ******* } 192 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 193 | constructor TDCSOAuth2Authenticator.Create(AOwner: TComponent); 194 | begin 195 | inherited Create(AOwner); 196 | self.ResetToDefaults; 197 | end; 198 | 199 | 200 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 201 | function TDCSOAuth2Authenticator.CreateBindSource: TBaseObjectBindSource; 202 | begin 203 | self.FBindSource := TDCSSubOAuth2AuthBindSource.Create(self); 204 | self.FBindSource.Name := 'BindSource'; { Do not localize } 205 | self.FBindSource.SetSubComponent(True); 206 | self.FBindSource.Authenticator := self; 207 | 208 | result := self.FBindSource; 209 | end; 210 | 211 | 212 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 213 | function TDCSOAuth2Authenticator.AccessTokenParamNameIsStored: Boolean; 214 | begin 215 | result := self.AccessTokenParamName <> DefaultOAuth2AccessTokenParamName; 216 | end; 217 | 218 | 219 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 220 | procedure TDCSOAuth2Authenticator.Assign(ASource: TDCSOAuth2Authenticator); 221 | begin 222 | self.ResetToDefaults; 223 | 224 | self.ClientID := ASource.ClientID; 225 | self.ClientSecret := ASource.ClientSecret; 226 | self.AuthCode := ASource.AuthCode; 227 | self.AccessToken := ASource.AccessToken; 228 | self.AccessTokenParamName := ASource.AccessTokenParamName; 229 | 230 | self.AccessTokenExpiry := ASource.AccessTokenExpiry; 231 | 232 | self.Scope := ASource.Scope; 233 | self.RefreshToken := ASource.RefreshToken; 234 | self.LocalState := ASource.LocalState; 235 | 236 | self.TokenType := ASource.TokenType; 237 | 238 | self.ResponseType := ASource.ResponseType; 239 | self.AuthorizationEndpoint := ASource.AuthorizationEndpoint; 240 | self.AccessTokenEndpoint := ASource.AccessTokenEndpoint; 241 | self.RedirectionEndpoint := ASource.RedirectionEndpoint; 242 | end; 243 | 244 | 245 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 246 | function TDCSOAuth2Authenticator.AuthorizationRequestURI: string; 247 | var 248 | respTypeStr: string; 249 | begin 250 | respTypeStr := DCSOAuth2ResponseTypeToString(self.FResponseType); 251 | 252 | result := self.FAuthorizationEndpoint; 253 | if true then result := result + '?response_type=' + URIEncode(respTypeStr); 254 | if self.FClientID <> '' then result := result + '&client_id=' + URIEncode(self.FClientID); 255 | if self.FRedirectionEndpoint <> '' then result := result + '&redirect_uri=' + URIEncode(self.FRedirectionEndpoint); 256 | if self.FScope <> '' then result := result + '&scope=' + URIEncode(self.FScope); 257 | if self.FLocalState <> '' then result := result + '&state=' + URIEncode(self.FLocalState); 258 | if self.FLoginHint <> '' then result := result + '&login_hint=' + URIEncode(self.FLoginHint); 259 | 260 | if self.FCodeChallenge <> '' then 261 | begin 262 | result := result + '&code_challenge_method=' + URIEncode('S256'); 263 | result := result + '&code_challenge=' + URIEncode(self.FCodeChallenge); 264 | end; 265 | end; 266 | 267 | 268 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 269 | // Get the tokens using the authorization code 270 | procedure TDCSOAuth2Authenticator.GetTokens_fromAuthCode; 271 | begin 272 | self.GetTokens(TDCSTokenRequestType.trtAuthGetTokens); 273 | end; 274 | 275 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 276 | // Get new tokens using the refresh token 277 | // - Call if the access token is expired 278 | procedure TDCSOAuth2Authenticator.GetTokens_fromRefreshToken; 279 | begin 280 | self.GetTokens(TDCSTokenRequestType.trtRefreshTokens); 281 | end; 282 | 283 | 284 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 285 | procedure TDCSOAuth2Authenticator.GetTokens(requestType: TDCSTokenRequestType); 286 | var 287 | restClient: TRestClient; 288 | restRequest: TRESTRequest; 289 | get_fromAuth: boolean; 290 | get_fremRefresh: boolean; 291 | respValueStr: string; 292 | expireSecs: int64; 293 | begin 294 | get_fromAuth := (requestType = TDCSTokenRequestType.trtAuthGetTokens); 295 | get_fremRefresh := (requestType = TDCSTokenRequestType.trtRefreshTokens); 296 | 297 | if get_fromAuth and (FAuthCode = '') then raise EOAuth2Exception.Create(SAuthorizationCodeNeeded); // AuthCode is needed to send it to the servce and exchange the code into an access-token 298 | if get_fremRefresh and (FRefreshToken = '') then raise EOAuth2Exception.Create('Empty RefreshToken'); // RefreshToken is needed to refresh the access-token 299 | 300 | restClient := TRestClient.Create(FAccessTokenEndpoint); 301 | try 302 | restRequest := TRESTRequest.Create(restClient); // The restClient now "owns" the Request and will free it. 303 | restRequest.Method := TRESTRequestMethod.rmPOST; 304 | 305 | // Add parameters to the request 306 | restRequest.AddAuthParameter('client_id', self.FClientID, TRESTRequestParameterKind.pkGETorPOST); 307 | restRequest.AddAuthParameter('client_secret', self.FClientSecret, TRESTRequestParameterKind.pkGETorPOST); 308 | restRequest.AddAuthParameter('redirect_uri', self.FRedirectionEndpoint, TRESTRequestParameterKind.pkGETorPOST); 309 | 310 | if get_fromAuth then 311 | begin 312 | restRequest.AddAuthParameter('code', self.FAuthCode, TRESTRequestParameterKind.pkGETorPOST); 313 | restRequest.AddAuthParameter('code_verifier', self.FCodeVerifier, TRESTRequestParameterKind.pkGETorPOST); // Added for PKCE 314 | restRequest.AddAuthParameter('grant_type', 'authorization_code', TRESTRequestParameterKind.pkGETorPOST); 315 | end else 316 | if get_fremRefresh then 317 | begin 318 | restRequest.AddAuthParameter('refresh_token', self.FRefreshToken, TRESTRequestParameterKind.pkGETorPOST); 319 | restRequest.AddAuthParameter('grant_type', 'refresh_token', TRESTRequestParameterKind.pkGETorPOST); 320 | end; 321 | 322 | // Make the request 323 | restRequest.Execute; 324 | 325 | // Get Tokens from response 326 | if restRequest.Response.GetSimpleValue('access_token', respValueStr) then self.FAccessToken := respValueStr; 327 | if restRequest.Response.GetSimpleValue('refresh_token', respValueStr) then self.FRefreshToken := respValueStr; 328 | if restRequest.Response.GetSimpleValue('token_type', respValueStr) then self.FTokenType := DCSOAuth2TokenTypeFromString(respValueStr); // token-type is important for how using it later on the normal requests to the API 329 | 330 | // Get token exipancy if provided by the service (value in secounds) 331 | if restRequest.Response.GetSimpleValue('expires_in', respValueStr) then 332 | begin 333 | expireSecs := StrToIntdef(respValueStr, -1); 334 | if (expireSecs > -1) 335 | then self.FAccessTokenExpiry := IncSecond(Now, expireSecs) 336 | else self.FAccessTokenExpiry := 0.0; 337 | end; 338 | 339 | // Clear AuthCode (can only be used once) 340 | if get_fromAuth and (self.FAccessToken <> '') then 341 | self.FAuthCode := ''; 342 | 343 | finally 344 | restClient.Free; 345 | end; 346 | end; 347 | 348 | 349 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 350 | // Request user authorization (using the browser) to get an authCode 351 | // Use the authCode to get the AccessToken and RefreshToken 352 | procedure TDCSOAuth2Authenticator.AquireAccessToken_browser; 353 | var 354 | i: integer; 355 | url: string; 356 | begin 357 | if (self.FClientID = '') then raise Exception.Create('ClientID required'); 358 | if (self.FClientSecret = '') then raise Exception.Create('ClientSecret required'); 359 | 360 | // Generate verification codes 361 | self.FLocalState := self.generate_randomString(10); // LocalState 362 | self.FCodeVerifier := self.generate_randomString(60); // PKCE 363 | self.FCodeChallenge := self.encode_SHA256_base64URL(FCodeVerifier); // PKCE 364 | 365 | // Get URL with queryString to open in the browser 366 | url := self.AuthorizationRequestURI; 367 | 368 | //******************* 369 | // Start Local Server 370 | // - the http server waits for the user to authorize 371 | // - then google redirects the browser to the local RedirectionEndpoint provided adding the AuthCode on its queryParams 372 | privTempAuthCode := ''; // Clear 373 | self.LS_start; 374 | 375 | //******************************* 376 | // Open link to get authorization 377 | ShellExecute(0, 'open', PChar(url), nil, nil, SW_SHOWNORMAL); 378 | 379 | //**************************************** 380 | // User will have 60 seconds to authorize 381 | for i := 0 to 60 do 382 | begin 383 | sleep(1000); 384 | 385 | if privTempAuthCode <> '' then // When this becomes set we have the auth code 386 | break; 387 | end; 388 | 389 | //****************** 390 | // Stop Local Server 391 | self.LS_stop; 392 | 393 | if privTempAuthCode <> K_invalidAuth then 394 | self.FAuthCode := privTempAuthCode; 395 | 396 | if (self.FAuthCode = '') 397 | then raise EOAuth2Exception.Create('Authentication failed'); 398 | 399 | //****************************** 400 | // Get Tokens using the AuthCode 401 | self.GetTokens_fromAuthCode(); 402 | 403 | if (self.FAccessToken = '') then 404 | raise EOAuth2Exception.Create('Failed to aquire access token'); 405 | end; 406 | 407 | 408 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 409 | function TDCSOAuth2Authenticator.getLocalRedirectionURL_andSetPort: string; 410 | begin 411 | self.privLS_port := self.LS_getFreePort; 412 | result := 'http://127.0.0.1:' + intToStr(self.privLS_port); 413 | end; 414 | 415 | 416 | 417 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 418 | function TDCSOAuth2Authenticator.LS_getFreePort: integer; 419 | begin 420 | with TServerSocket.Create(self) do 421 | begin 422 | Port := 0; 423 | Active := true; 424 | result := Socket.LocalPort; 425 | Active := false; 426 | 427 | Free; 428 | end; 429 | end; 430 | 431 | 432 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 433 | procedure TDCSOAuth2Authenticator.LS_start; 434 | begin 435 | if privLS <> nil then raise Exception.Create('Error on LS_start'); 436 | 437 | privLS := TIdHTTPServer.Create(nil); 438 | 439 | privLS.Active := false; 440 | privLS.DefaultPort := self.privLS_port; 441 | privLS.OnCommandGet := LS_onCommandGet; 442 | privLS.OnCommandError := LS_onCommandError; 443 | privLS.Active := true; 444 | end; 445 | 446 | 447 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 448 | procedure TDCSOAuth2Authenticator.LS_stop; 449 | begin 450 | privLS.Active := false; 451 | FreeAndNil(privLS); 452 | end; 453 | 454 | 455 | 456 | 457 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 458 | // This event runs when the Local Server processes a GET request 459 | // - When the user accepts (or not) the request for authorization in the browser 460 | // the service (Google) calls the redirect URL provided earlier 461 | // - In this case we the localhost URL 462 | // - The service adds the AuthCode to the URL with a query string named "code" 463 | procedure TDCSOAuth2Authenticator.LS_onCommandGet(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo); 464 | var 465 | getCode: boolean; 466 | errorHtml: string; 467 | codeStr: string; 468 | stateStr: string; 469 | begin 470 | 471 | if ARequestInfo.QueryParams = '' then exit; // The request has to have query strings 472 | if privTempAuthCode <> '' then exit; // Exit if the AuthCode was already captured 473 | 474 | // Obter erro caso exista 475 | errorHtml := ARequestInfo.Params.Values['error']; 476 | getCode := (errorHtml = ''); 477 | 478 | //*************** 479 | // Obter AuthCode 480 | if getCode then 481 | begin 482 | codeStr := ARequestInfo.Params.Values['code']; 483 | stateStr := ARequestInfo.Params.Values['state']; 484 | 485 | if stateStr = self.FLocalState then // Value LocalState was sent to the browser and have to return unchanged 486 | privTempAuthCode := codeStr; 487 | end; 488 | 489 | 490 | if privTempAuthCode = '' then 491 | privTempAuthCode := K_invalidAuth; 492 | 493 | //*********************************** 494 | // Set HTML response (to the browser) 495 | if (privTempAuthCode = K_invalidAuth) and (errorHtml = '') then 496 | errorHtml := 'Auth code not found'; 497 | 498 | if privTempAuthCode = K_invalidAuth then AResponseInfo.ContentText := self.LS_getMsgPage_html(errorHtml) 499 | else AResponseInfo.ContentText := self.LS_getMsgPage_html(''); 500 | end; 501 | 502 | 503 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 504 | procedure TDCSOAuth2Authenticator.LS_onCommandError(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo; AException: Exception); 505 | begin 506 | privTempAuthCode := K_invalidAuth; 507 | raise EOAuth2Exception.Create('LS_onCommandError: ' + AException.Message); 508 | end; 509 | 510 | 511 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 512 | function TDCSOAuth2Authenticator.LS_getMsgPage_html(errorStr: string = ''): string; 513 | resourcestring 514 | RS_LS_page_ok = 'Authorization OK'; 515 | RS_LS_page_error = 'Authorization Fail'; 516 | RS_LS_ok_tit = 'The authorization has succeeded'; 517 | RS_LS_ok_msg = 'You can close this page and return to the application.'; 518 | RS_LS_error_tit = 'Something went wrong...'; 519 | RS_LS_error_msg = 'You can close this page and return to the application to try again.'; 520 | const 521 | K_HTML_doc = '%s' + 522 | '

 

 

' + 523 | '
' + 524 | '%s' + 525 | '
' + 526 | '

 

'; 527 | K_HTML_h3_green = '

%s

'; 528 | K_HTML_h3_red = '

%s

'; 529 | K_HTML_P = '

%s

'; 530 | var 531 | errorPage: boolean; 532 | h3_html: string; 533 | p1_html: string; 534 | p2_html: string; 535 | page_title: string; 536 | page_content: string; 537 | begin 538 | errorPage := errorStr <> ''; 539 | 540 | if errorPage 541 | then begin 542 | h3_html := THTMLEncoding.HTML.Encode(RS_LS_error_tit); 543 | p1_html := THTMLEncoding.HTML.Encode(RS_LS_error_msg); 544 | p2_html := THTMLEncoding.HTML.Encode(errorStr); 545 | 546 | h3_html := format(K_HTML_h3_red, [h3_html]); 547 | p1_html := format(K_HTML_p, [p1_html]); 548 | p2_html := format(K_HTML_p, [p2_html]); 549 | 550 | page_title := RS_LS_page_error; 551 | page_content := h3_html + p1_html + p2_html; 552 | end 553 | else begin 554 | h3_html := THTMLEncoding.HTML.Encode(RS_LS_ok_tit); 555 | p1_html := THTMLEncoding.HTML.Encode(RS_LS_ok_msg); 556 | 557 | h3_html := format(K_HTML_h3_green, [h3_html]); 558 | p1_html := format(K_HTML_p, [p1_html]); 559 | 560 | page_title := RS_LS_page_ok; 561 | page_content := h3_html + p1_html; 562 | end; 563 | 564 | result := format(K_HTML_doc, [page_title, page_content]); 565 | end; 566 | 567 | 568 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 569 | function TDCSOAuth2Authenticator.generate_randomString(strLength: integer): string; 570 | const 571 | K_charsToUse = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-._~'; 572 | var 573 | i: integer; 574 | allChars_count: integer; 575 | curCharPos: byte; 576 | begin 577 | allChars_count := Length(K_charsToUse); 578 | 579 | SetLength(result, strLength); 580 | Randomize; 581 | 582 | for i := 1 to strLength do 583 | begin 584 | curCharPos := Random(allChars_count) + 1; // +1 becouse strings start in 1 and Random generates values of 0 <= X < Range 585 | result[i] := K_charsToUse[curCharPos]; 586 | end; 587 | end; 588 | 589 | 590 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 591 | function TDCSOAuth2Authenticator.encode_SHA256_base64URL(str_toEncode: string): string; 592 | var 593 | hash_sha256: TIdHashSHA256; 594 | enc_base64: TBase64Encoding; 595 | arr_sha256: TIdBytes; 596 | str_b64: string; 597 | str_b64URL: string; 598 | begin 599 | result := ''; 600 | 601 | LoadOpenSSLLibrary; 602 | if not TIdHashSHA256.IsAvailable then raise Exception.Create('Error encode_SHA256_base64URL: TIdHashSHA256 not available.'); 603 | 604 | hash_sha256 := TIdHashSHA256.Create; 605 | enc_base64 := TBase64Encoding.Create(0); 606 | 607 | try 608 | arr_sha256 := hash_sha256.HashString(str_toEncode, IndyTextEncoding_ASCII); // Hash SHA256 609 | str_b64 := enc_base64.EncodeBytesToString(arr_sha256); // Convert SHA256 hash to Base64 610 | 611 | // Convert Base64 to Base64URL 612 | str_b64URL := str_b64; 613 | str_b64URL := StringReplace(str_b64URL, '+', '-', [rfReplaceAll]); // Replace + with - 614 | str_b64URL := StringReplace(str_b64URL, '/', '_', [rfReplaceAll]); // Replace / with _ 615 | str_b64URL := StringReplace(str_b64URL, '=', '', [rfReplaceAll]); // Remove padding, character = 616 | 617 | result := str_b64URL; 618 | finally 619 | enc_base64.Free; 620 | hash_sha256.Free; 621 | end; 622 | end; 623 | 624 | 625 | 626 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 627 | procedure TDCSOAuth2Authenticator.DefineProperties(Filer: TFiler); 628 | begin 629 | inherited; 630 | 631 | Filer.DefineProperty('AccessTokenExpiryDate', 632 | self.ReadAccessTokenExpiryData, 633 | self.WriteAccessTokenExpiryData, 634 | (self.FAccessTokenExpiry > 0.1)); 635 | end; 636 | 637 | 638 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 639 | // The procedure runs on every API request 640 | // Flow: 641 | // 1. If no access_token is defined, the browser auth process is started 642 | // 2. If tokens are expired, get new ones using the RefreshToken 643 | // 3. Add the access_token to the request 644 | procedure TDCSOAuth2Authenticator.DoAuthenticate(ARequest: TCustomRESTRequest); 645 | var 646 | accessParamName: string; 647 | begin 648 | inherited; 649 | 650 | // Get or refresh the tokens if needed 651 | if self.FAccessToken = '' then self.AquireAccessToken_browser; 652 | if self.FAccessTokenExpiry < now then self.GetTokens_fromRefreshToken; 653 | 654 | // Use another parameter name for the access_token if necessary 655 | // - Only used when the token type is not Bearer 656 | accessParamName := self.FAccessTokenParamName; 657 | if (Trim(accessParamName) = '') then 658 | accessParamName := DefaultOAuth2AccessTokenParamName; 659 | 660 | // Add AccessToken to the request 661 | if self.FTokenType = TDCSOAuth2TokenType.ttBEARER 662 | then ARequest.AddAuthParameter(HTTP_HEADERFIELD_AUTH, 'Bearer ' + self.FAccessToken, TRESTRequestParameterKind.pkHTTPHEADER, [TRESTRequestParameterOption.poDoNotEncode]) 663 | else ARequest.AddAuthParameter(accessParamName, self.FAccessToken, TRESTRequestParameterKind.pkGETorPOST, [TRESTRequestParameterOption.poDoNotEncode]); 664 | end; 665 | 666 | 667 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 668 | procedure TDCSOAuth2Authenticator.ReadAccessTokenExpiryData(AReader: TReader); 669 | begin 670 | self.FAccessTokenExpiry := AReader.ReadDate; 671 | end; 672 | 673 | 674 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 675 | procedure TDCSOAuth2Authenticator.ResetToDefaults; 676 | begin 677 | inherited; 678 | 679 | self.AuthorizationEndpoint := ''; 680 | self.AccessTokenEndpoint := ''; 681 | self.RedirectionEndpoint := ''; 682 | 683 | self.ClientID := ''; 684 | self.ClientSecret := ''; 685 | self.AuthCode := ''; 686 | self.AccessToken := ''; 687 | self.FAccessTokenExpiry := 0.0; 688 | self.Scope := ''; 689 | self.RefreshToken := ''; 690 | self.LocalState := ''; 691 | self.LoginHint := ''; 692 | self.CodeVerifier := ''; 693 | self.CodeChallenge := ''; 694 | 695 | self.FTokenType := DefaultOAuth2TokenType; 696 | self.ResponseType := DefaultOAuth2ResponseType; 697 | self.AccessTokenParamName := DefaultOAuth2AccessTokenParamName; 698 | end; 699 | 700 | 701 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 702 | function TDCSOAuth2Authenticator.ResponseTypeIsStored: Boolean; 703 | begin 704 | Result := self.ResponseType <> DefaultOAuth2ResponseType; 705 | end; 706 | 707 | 708 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 709 | procedure TDCSOAuth2Authenticator.SetAccessToken(const AValue: string); 710 | begin 711 | if AValue <> FAccessToken then 712 | begin 713 | FAccessToken := AValue; 714 | PropertyValueChanged; 715 | end; 716 | end; 717 | 718 | 719 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 720 | procedure TDCSOAuth2Authenticator.SetAccessTokenEndpoint(const AValue: string); 721 | begin 722 | if AValue <> FAccessTokenEndpoint then 723 | begin 724 | FAccessTokenEndpoint := AValue; 725 | PropertyValueChanged; 726 | end; 727 | end; 728 | 729 | 730 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 731 | procedure TDCSOAuth2Authenticator.SetAccessTokenExpiry(const AExpiry: TDateTime); 732 | begin 733 | if AExpiry <> FAccessTokenExpiry then 734 | begin 735 | FAccessTokenExpiry := AExpiry; 736 | PropertyValueChanged; 737 | end; 738 | end; 739 | 740 | 741 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 742 | procedure TDCSOAuth2Authenticator.SetAccessTokenParamName(const AValue: string); 743 | begin 744 | if AValue <> FAccessTokenParamName then 745 | begin 746 | FAccessTokenParamName := AValue; 747 | PropertyValueChanged; 748 | end; 749 | end; 750 | 751 | 752 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 753 | procedure TDCSOAuth2Authenticator.SetAuthCode(const AValue: string); 754 | begin 755 | if AValue <> FAuthCode then 756 | begin 757 | FAuthCode := AValue; 758 | PropertyValueChanged; 759 | end; 760 | end; 761 | 762 | 763 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 764 | procedure TDCSOAuth2Authenticator.SetAuthorizationEndpoint(const AValue: string); 765 | begin 766 | if AValue <> FAuthorizationEndpoint then 767 | begin 768 | FAuthorizationEndpoint := AValue; 769 | PropertyValueChanged; 770 | end; 771 | end; 772 | 773 | 774 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 775 | procedure TDCSOAuth2Authenticator.SetClientID(const AValue: string); 776 | begin 777 | if AValue <> FClientID then 778 | begin 779 | FClientID := AValue; 780 | PropertyValueChanged; 781 | end; 782 | end; 783 | 784 | 785 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 786 | procedure TDCSOAuth2Authenticator.SetClientSecret(const AValue: string); 787 | begin 788 | if AValue <> FClientSecret then 789 | begin 790 | FClientSecret := AValue; 791 | PropertyValueChanged; 792 | end; 793 | end; 794 | 795 | 796 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 797 | procedure TDCSOAuth2Authenticator.SetLocalState(const AValue: string); 798 | begin 799 | if AValue <> FLocalState then 800 | begin 801 | FLocalState := AValue; 802 | PropertyValueChanged; 803 | end; 804 | end; 805 | 806 | 807 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 808 | procedure TDCSOAuth2Authenticator.SetRedirectionEndpoint(const AValue: string); 809 | begin 810 | if AValue <> FRedirectionEndpoint then 811 | begin 812 | FRedirectionEndpoint := AValue; 813 | PropertyValueChanged; 814 | end; 815 | end; 816 | 817 | 818 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 819 | procedure TDCSOAuth2Authenticator.SetRefreshToken(const AValue: string); 820 | begin 821 | if AValue <> FRefreshToken then 822 | begin 823 | FRefreshToken := AValue; 824 | PropertyValueChanged; 825 | end; 826 | end; 827 | 828 | 829 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 830 | procedure TDCSOAuth2Authenticator.SetResponseType(const AValue: TDCSOAuth2ResponseType); 831 | begin 832 | if AValue <> FResponseType then 833 | begin 834 | FResponseType := AValue; 835 | PropertyValueChanged; 836 | end; 837 | end; 838 | 839 | 840 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 841 | procedure TDCSOAuth2Authenticator.SetScope(const AValue: string); 842 | begin 843 | if AValue <> FScope then 844 | begin 845 | FScope := AValue; 846 | PropertyValueChanged; 847 | end; 848 | end; 849 | 850 | 851 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 852 | procedure TDCSOAuth2Authenticator.SetTokenType(const AType: TDCSOAuth2TokenType); 853 | begin 854 | if AType <> FTokenType then 855 | begin 856 | FTokenType := AType; 857 | PropertyValueChanged; 858 | end; 859 | end; 860 | 861 | 862 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 863 | function TDCSOAuth2Authenticator.TokenTypeIsStored: Boolean; 864 | begin 865 | Result := TokenType <> DefaultOAuth2TokenType; 866 | end; 867 | 868 | 869 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 870 | procedure TDCSOAuth2Authenticator.WriteAccessTokenExpiryData(AWriter: TWriter); 871 | begin 872 | AWriter.WriteDate(FAccessTokenExpiry); 873 | end; 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 882 | { ******* // // ******* } 883 | { ******* // TDCSSubOAuth2AuthBindSource // ******* } 884 | { ******* // // ******* } 885 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 886 | function TDCSSubOAuth2AuthBindSource.CreateAdapterT: TRESTAuthenticatorAdapter; 887 | begin 888 | result := TDCSOAuth2AuthAdapter.Create(self); 889 | end; 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 898 | { ******* // // ******* } 899 | { ******* // TDCSOAuth2AuthAdapter // ******* } 900 | { ******* // // ******* } 901 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 902 | procedure TDCSOAuth2AuthAdapter.AddFields; 903 | const 904 | sAccessToken = 'AccessToken'; 905 | sAccessTokenEndpoint = 'AccessTokenEndpoint'; 906 | sRefreshToken = 'RefreshToken'; 907 | sAuthCode = 'AuthCode'; 908 | sClientID = 'ClientID'; 909 | sClientSecret = 'ClientSecret'; 910 | sAuthorizationEndpoint = 'AuthorizationEndpoint'; 911 | sRedirectionEndpoint = 'RedirectionEndpoint'; 912 | sScope = 'Scope'; 913 | sLocalState = 'LocalState'; 914 | sCodeVerifier = 'CodeVerifier'; 915 | sCodeChallenge = 'CodeChallenge'; 916 | sLoginHint = 'LoginHint'; 917 | var 918 | LGetMemberObject: IGetMemberObject; 919 | begin 920 | CheckInactive; 921 | ClearFields; 922 | if Authenticator <> nil then 923 | begin 924 | LGetMemberObject := TBindSourceAdapterGetMemberObject.Create(self); 925 | 926 | CreateReadWriteField(sAccessToken, LGetMemberObject, TScopeMemberType.mtText, 927 | function: string 928 | begin 929 | result := Authenticator.AccessToken; 930 | end, 931 | procedure(AValue: string) 932 | begin 933 | Authenticator.AccessToken := AValue; 934 | end); 935 | 936 | CreateReadWriteField(sAccessTokenEndpoint, LGetMemberObject, TScopeMemberType.mtText, 937 | function: string 938 | begin 939 | result := Authenticator.AccessTokenEndpoint; 940 | end, 941 | procedure(AValue: string) 942 | begin 943 | Authenticator.AccessTokenEndpoint := AValue; 944 | end); 945 | 946 | CreateReadWriteField(sRefreshToken, LGetMemberObject, TScopeMemberType.mtText, 947 | function: string 948 | begin 949 | result := Authenticator.RefreshToken; 950 | end, 951 | procedure(AValue: string) 952 | begin 953 | Authenticator.RefreshToken := AValue; 954 | end); 955 | 956 | CreateReadWriteField(sAuthCode, LGetMemberObject, TScopeMemberType.mtText, 957 | function: string 958 | begin 959 | result := Authenticator.AuthCode; 960 | end, 961 | procedure(AValue: string) 962 | begin 963 | Authenticator.AuthCode := AValue; 964 | end); 965 | 966 | CreateReadWriteField(sClientID, LGetMemberObject, TScopeMemberType.mtText, 967 | function: string 968 | begin 969 | result := Authenticator.ClientID; 970 | end, 971 | procedure(AValue: string) 972 | begin 973 | Authenticator.ClientID := AValue; 974 | end); 975 | 976 | CreateReadWriteField(sClientSecret, LGetMemberObject, TScopeMemberType.mtText, 977 | function: string 978 | begin 979 | result := Authenticator.ClientSecret; 980 | end, 981 | procedure(AValue: string) 982 | begin 983 | Authenticator.ClientSecret := AValue; 984 | end); 985 | 986 | CreateReadWriteField(sAuthorizationEndpoint, LGetMemberObject, TScopeMemberType.mtText, 987 | function: string 988 | begin 989 | result := Authenticator.AuthorizationEndpoint; 990 | end, 991 | procedure(AValue: string) 992 | begin 993 | Authenticator.AuthorizationEndpoint := AValue; 994 | end); 995 | 996 | CreateReadWriteField(sRedirectionEndpoint, LGetMemberObject, TScopeMemberType.mtText, 997 | function: string 998 | begin 999 | result := Authenticator.RedirectionEndpoint; 1000 | end, 1001 | procedure(AValue: string) 1002 | begin 1003 | Authenticator.RedirectionEndpoint := AValue; 1004 | end); 1005 | 1006 | CreateReadWriteField(sScope, LGetMemberObject, TScopeMemberType.mtText, 1007 | function: string 1008 | begin 1009 | result := Authenticator.Scope; 1010 | end, 1011 | procedure(AValue: string) 1012 | begin 1013 | Authenticator.Scope := AValue; 1014 | end); 1015 | 1016 | CreateReadWriteField(sLocalState, LGetMemberObject, TScopeMemberType.mtText, 1017 | function: string 1018 | begin 1019 | result := Authenticator.LocalState; 1020 | end, 1021 | procedure(AValue: string) 1022 | begin 1023 | Authenticator.LocalState := AValue; 1024 | end); 1025 | 1026 | CreateReadWriteField(sCodeVerifier, LGetMemberObject, TScopeMemberType.mtText, 1027 | function: string 1028 | begin 1029 | result := Authenticator.CodeVerifier; 1030 | end, 1031 | procedure(AValue: string) 1032 | begin 1033 | Authenticator.CodeVerifier := AValue; 1034 | end); 1035 | 1036 | CreateReadWriteField(sCodeChallenge, LGetMemberObject, TScopeMemberType.mtText, 1037 | function: string 1038 | begin 1039 | result := Authenticator.CodeChallenge; 1040 | end, 1041 | procedure(AValue: string) 1042 | begin 1043 | Authenticator.CodeChallenge := AValue; 1044 | end); 1045 | 1046 | CreateReadWriteField(sLoginHint, LGetMemberObject, TScopeMemberType.mtText, 1047 | function: string 1048 | begin 1049 | result := Authenticator.LoginHint; 1050 | end, 1051 | procedure(AValue: string) 1052 | begin 1053 | Authenticator.LoginHint := AValue; 1054 | end); 1055 | end; 1056 | end; 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | 1063 | 1064 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 1065 | { ******* // // ******* } 1066 | { ******* // Unit functions // ******* } 1067 | { ******* // // ******* } 1068 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 1069 | 1070 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 1071 | function DCSOAuth2ResponseTypeToString(const AType: TDCSOAuth2ResponseType): string; 1072 | begin 1073 | case AType of 1074 | TDCSOAuth2ResponseType.rtCODE: result := 'code'; // do not localize 1075 | TDCSOAuth2ResponseType.rtTOKEN: result := 'token'; // do not localize 1076 | else 1077 | result := ''; 1078 | end; 1079 | end; 1080 | 1081 | 1082 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 1083 | function DCSOAuth2ResponseTypeFromString(const ATypeString: string): TDCSOAuth2ResponseType; 1084 | var 1085 | LType: TDCSOAuth2ResponseType; 1086 | begin 1087 | result := DefaultOAuth2ResponseType; 1088 | 1089 | for LType IN [Low(TDCSOAuth2ResponseType)..High(TDCSOAuth2ResponseType)] do 1090 | if SameText(ATypeString, DCSOAuth2ResponseTypeToString(LType)) then 1091 | begin 1092 | result := LType; 1093 | BREAK; 1094 | end; 1095 | end; 1096 | 1097 | 1098 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 1099 | function DCSOAuth2TokenTypeToString(const AType: TDCSOAuth2TokenType): string; 1100 | begin 1101 | case AType of 1102 | TDCSOAuth2TokenType.ttBEARER: result := 'bearer'; // do not localize 1103 | else 1104 | result := ''; 1105 | end; 1106 | end; 1107 | 1108 | 1109 | { ******* // ******* // ******* // ******* // ******* // ******* // ******* } 1110 | function DCSOAuth2TokenTypeFromString(const ATypeString: string): TDCSOAuth2TokenType; 1111 | var 1112 | LType: TDCSOAuth2TokenType; 1113 | begin 1114 | result := DefaultOAuth2TokenType; 1115 | 1116 | for LType IN [Low(TDCSOAuth2TokenType) .. High(TDCSOAuth2TokenType)] do 1117 | if SameText(ATypeString, DCSOAuth2TokenTypeToString(LType)) then 1118 | begin 1119 | result := LType; 1120 | BREAK; 1121 | end; 1122 | end; 1123 | 1124 | 1125 | initialization 1126 | 1127 | end. 1128 | -------------------------------------------------------------------------------- /U_emailExample.dfm: -------------------------------------------------------------------------------- 1 | object FRM_sendMail: TFRM_sendMail 2 | Left = 0 3 | Top = 0 4 | Caption = 'FRM_sendMail' 5 | ClientHeight = 291 6 | ClientWidth = 352 7 | Color = clBtnFace 8 | Constraints.MinHeight = 300 9 | Constraints.MinWidth = 360 10 | Font.Charset = DEFAULT_CHARSET 11 | Font.Color = clWindowText 12 | Font.Height = -11 13 | Font.Name = 'Tahoma' 14 | Font.Style = [] 15 | OldCreateOrder = False 16 | Position = poScreenCenter 17 | OnCreate = FormCreate 18 | OnDestroy = FormDestroy 19 | DesignSize = ( 20 | 352 21 | 291) 22 | PixelsPerInch = 96 23 | TextHeight = 13 24 | object Label1: TLabel 25 | Left = 17 26 | Top = 27 27 | Width = 65 28 | Height = 13 29 | Alignment = taRightJustify 30 | Caption = 'Sender email:' 31 | end 32 | object Label2: TLabel 33 | Left = 88 34 | Top = 46 35 | Width = 240 36 | Height = 13 37 | Anchors = [akLeft, akTop, akRight] 38 | Caption = '(email that will use Google API to send email)' 39 | ExplicitWidth = 214 40 | end 41 | object Label3: TLabel 42 | Left = 14 43 | Top = 75 44 | Width = 68 45 | Height = 13 46 | Alignment = taRightJustify 47 | Caption = 'Send email to:' 48 | end 49 | object Label4: TLabel 50 | Left = 42 51 | Top = 102 52 | Width = 40 53 | Height = 13 54 | Alignment = taRightJustify 55 | Caption = 'Subject:' 56 | end 57 | object Label5: TLabel 58 | Left = 36 59 | Top = 126 60 | Width = 46 61 | Height = 13 62 | Alignment = taRightJustify 63 | Caption = 'Message:' 64 | end 65 | object EDT_email_google: TEdit 66 | Left = 88 67 | Top = 24 68 | Width = 243 69 | Height = 21 70 | Anchors = [akLeft, akTop, akRight] 71 | TabOrder = 0 72 | Text = '@gmail.com' 73 | ExplicitWidth = 217 74 | end 75 | object EDT_toEmail: TEdit 76 | Left = 88 77 | Top = 72 78 | Width = 243 79 | Height = 21 80 | Anchors = [akLeft, akTop, akRight] 81 | TabOrder = 1 82 | ExplicitWidth = 217 83 | end 84 | object EDT_toSubject: TEdit 85 | Left = 88 86 | Top = 99 87 | Width = 243 88 | Height = 21 89 | Anchors = [akLeft, akTop, akRight] 90 | TabOrder = 2 91 | Text = 'Email subject' 92 | ExplicitWidth = 217 93 | end 94 | object MEM_toMessage: TMemo 95 | Left = 88 96 | Top = 126 97 | Width = 243 98 | Height = 114 99 | Anchors = [akLeft, akTop, akRight, akBottom] 100 | Lines.Strings = ( 101 | 'Email message.') 102 | TabOrder = 3 103 | ExplicitWidth = 217 104 | ExplicitHeight = 89 105 | end 106 | object BUT_send: TButton 107 | Left = 256 108 | Top = 246 109 | Width = 75 110 | Height = 25 111 | Anchors = [akRight, akBottom] 112 | Caption = 'Send' 113 | TabOrder = 4 114 | OnClick = BUT_sendClick 115 | ExplicitLeft = 230 116 | ExplicitTop = 221 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /U_emailExample.pas: -------------------------------------------------------------------------------- 1 | unit U_emailExample; 2 | 3 | interface 4 | 5 | uses 6 | Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, 7 | Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, 8 | U_DCS_OAuth2, IdBaseComponent, IdMessage; 9 | 10 | const 11 | K_tokens_file = 'tokens.txt'; 12 | K_token_expirancy = 'token_expirancy'; 13 | K_token_access = 'token_access'; 14 | K_token_refresh = 'token_refresh'; 15 | 16 | type 17 | TFRM_sendMail = class(TForm) 18 | EDT_email_google: TEdit; 19 | Label1: TLabel; 20 | Label2: TLabel; 21 | Label3: TLabel; 22 | EDT_toEmail: TEdit; 23 | Label4: TLabel; 24 | EDT_toSubject: TEdit; 25 | MEM_toMessage: TMemo; 26 | Label5: TLabel; 27 | BUT_send: TButton; 28 | procedure FormCreate(Sender: TObject); 29 | procedure BUT_sendClick(Sender: TObject); 30 | procedure FormDestroy(Sender: TObject); 31 | private 32 | { Private declarations } 33 | TSL_tokens: tStringList; 34 | 35 | privAppPath: string; 36 | DCSOAuth2Authenticator: TDCSOAuth2Authenticator; 37 | 38 | procedure prepareAuthenticator(senderEmail: string; clearTokens: boolean = false); 39 | procedure emailSender_send_viaGmail; 40 | function getEmailMessage_fromForm: tIdMessage; 41 | 42 | procedure loadOptions; 43 | procedure saveOptions; 44 | public 45 | { Public declarations } 46 | end; 47 | 48 | var 49 | FRM_sendMail: TFRM_sendMail; 50 | 51 | implementation 52 | 53 | {$R *.dfm} 54 | 55 | uses 56 | REST.Client, REST.Types, System.JSON, Web.HTTPApp, IdText, dateUtils; 57 | 58 | 59 | { ***** / / ***** / / ****** / / ***** } 60 | procedure TFRM_sendMail.FormCreate(Sender: TObject); 61 | begin 62 | TSL_tokens := tStringList.Create; 63 | DCSOAuth2Authenticator := TDCSOAuth2Authenticator.Create(nil); 64 | 65 | privAppPath := Application.ExeName; 66 | privAppPath := ExtractFilePath(privAppPath); 67 | 68 | self.loadOptions; 69 | end; 70 | 71 | 72 | { ***** / / ***** / / ****** / / ***** } 73 | procedure TFRM_sendMail.FormDestroy(Sender: TObject); 74 | begin 75 | self.saveOptions; 76 | 77 | DCSOAuth2Authenticator.Free; 78 | TSL_tokens.Free; 79 | end; 80 | 81 | 82 | { ***** / / ***** / / ****** / / ***** } 83 | procedure TFRM_sendMail.BUT_sendClick(Sender: TObject); 84 | begin 85 | self.emailSender_send_viaGmail; 86 | 87 | ShowMessage('The email was sent.'); 88 | end; 89 | 90 | 91 | { ***** / / ***** / / ****** / / ***** } 92 | procedure TFRM_sendMail.loadOptions; 93 | var 94 | unix_exp: Int64; 95 | unix_onFile: string; 96 | begin 97 | if FileExists(privAppPath + K_tokens_file) then 98 | begin 99 | // Open tokens file 100 | TSL_tokens.LoadFromFile(privAppPath + K_tokens_file); 101 | 102 | // Load previousely obtained tokens 103 | unix_onFile := TSL_tokens.Values[K_token_expirancy]; 104 | if unix_onFile = '' 105 | then unix_exp := DateTimeToUnix(now, false) 106 | else unix_exp := StrToInt64(unix_onFile); 107 | 108 | DCSOAuth2Authenticator.AccessToken := TSL_tokens.Values[K_token_access]; 109 | DCSOAuth2Authenticator.RefreshToken := TSL_tokens.Values[K_token_refresh]; 110 | DCSOAuth2Authenticator.AccessTokenExpiry := UnixToDateTime(unix_exp, false); 111 | 112 | // Load other options 113 | EDT_email_google.Text := TSL_tokens.Values[EDT_email_google.Name]; 114 | EDT_toEmail.Text := TSL_tokens.Values[EDT_toEmail.Name]; 115 | EDT_toSubject.Text := TSL_tokens.Values[EDT_toSubject.Name]; 116 | end; 117 | end; 118 | 119 | 120 | { ***** / / ***** / / ****** / / ***** } 121 | procedure TFRM_sendMail.saveOptions; 122 | var 123 | unix_exp: Int64; 124 | unix_expStr: string; 125 | begin 126 | // Save corrent tokens 127 | unix_exp := DateTimeToUnix(DCSOAuth2Authenticator.AccessTokenExpiry, false); 128 | unix_expStr := IntToStr(unix_exp); 129 | TSL_tokens.Values[K_token_access] := DCSOAuth2Authenticator.AccessToken; 130 | TSL_tokens.Values[K_token_refresh] := DCSOAuth2Authenticator.RefreshToken; 131 | TSL_tokens.Values[K_token_expirancy] := unix_expStr; 132 | 133 | // Save other options 134 | TSL_tokens.Values[EDT_email_google.Name] := EDT_email_google.Text; 135 | TSL_tokens.Values[EDT_toEmail.Name] := EDT_toEmail.Text; 136 | TSL_tokens.Values[EDT_toSubject.Name] := EDT_toSubject.Text; 137 | 138 | // Save to tokens file 139 | TSL_tokens.SaveToFile(privAppPath + K_tokens_file); 140 | end; 141 | 142 | 143 | { ***** / / ***** / / ****** / / ***** } 144 | procedure TFRM_sendMail.prepareAuthenticator(senderEmail: string; clearTokens: boolean = false); 145 | begin 146 | if clearTokens then 147 | DCSOAuth2Authenticator.ResetToDefaults; // Reset tokens 148 | 149 | // General options 150 | DCSOAuth2Authenticator.AccessTokenEndpoint := 'https://www.googleapis.com/oauth2/v4/token'; 151 | DCSOAuth2Authenticator.AuthorizationEndpoint := 'https://accounts.google.com/o/oauth2/v2/auth'; 152 | DCSOAuth2Authenticator.ResponseType := TDCSOAuth2ResponseType.rtCODE; 153 | DCSOAuth2Authenticator.Scope := 'https://www.googleapis.com/auth/gmail.send'; 154 | DCSOAuth2Authenticator.RedirectionEndpoint := DCSOAuth2Authenticator.getLocalRedirectionURL_andSetPort; 155 | 156 | // Application specific options (created on Google's console) 157 | DCSOAuth2Authenticator.ClientID := 'your ClientID goes here'; // ClientID created on console.developers.google.com 158 | DCSOAuth2Authenticator.ClientSecret := 'your ClientSecret goes here'; // ClientSecret for the application registered on console.developers.google.com 159 | 160 | // Email hint 161 | DCSOAuth2Authenticator.LoginHint := senderEmail; 162 | end; 163 | 164 | 165 | { ***** / / ***** / / ****** / / ***** } 166 | procedure TFRM_sendMail.emailSender_send_viaGmail; 167 | var 168 | restClient: TRestClient; 169 | restRequest: TRESTRequest; 170 | fromChanged: boolean; 171 | endPoint: string; 172 | fromEmail: string; 173 | MSG_email: TIdMessage; 174 | msgStream: tMemoryStream; 175 | errJSON_Obj: TJSonObject; 176 | errJSONValue: TJSonValue; 177 | errorStr: string; 178 | begin 179 | MSG_email := self.getEmailMessage_fromForm; 180 | fromChanged := TSL_tokens.Values[EDT_email_google.Name] <> EDT_email_google.Text; 181 | fromEmail := MSG_email.From.Address; 182 | msgStream := tMemoryStream.Create; 183 | 184 | // IMPORTANT: sender email in the URL 185 | endPoint := format('https://gmail.googleapis.com/upload/gmail/v1/users/%s/messages/send', [fromEmail]); 186 | 187 | restClient := TRestClient.Create(endPoint); 188 | restRequest := TRESTRequest.Create(restClient); 189 | 190 | errJSON_Obj := TJSonObject.Create; 191 | errJSONValue := nil; 192 | 193 | self.prepareAuthenticator(fromEmail, fromChanged); // Clear tokens if From email changed 194 | restClient.Authenticator := DCSOAuth2Authenticator; 195 | 196 | try 197 | if MSG_email.BccList.Count <= 0 198 | then MSG_email.SaveToStream(msgStream, false) 199 | else begin 200 | MSG_email.SaveToFile (privAppPath + 'tmp.eml'); // Limitation of Indy, when bcc is set TIdMessage.SaveToStream loses that field. We use TIdMessage.SaveToFile and then stream.LoadFromFile to get arround that problem 201 | msgStream.LoadFromFile(privAppPath + 'tmp.eml'); 202 | end; 203 | 204 | // Add email headers 205 | restRequest.Method := TRESTRequestMethod.rmPOST; 206 | restRequest.Params.AddHeader('Content-Type', htmlEncode('message/rfc822')); 207 | restRequest.Params.ParameterByName('Content-Type').Options := [poDoNotEncode]; 208 | restRequest.AddParameter('uploadType', 'media', pkQUERY); 209 | 210 | restRequest.Body.Add(msgStream, ctMESSAGE_RFC822); // If email with metadata only, use: //restRequest.Body.Add(format('{"raw": "%s"}', [MEM_base64.Lines.Text]), ctMESSAGE_RFC822); 211 | 212 | //************* 213 | // Send request 214 | restRequest.Execute; 215 | 216 | if fromChanged then 217 | self.saveOptions; 218 | 219 | // If Error response 220 | if restRequest.Response.GetSimpleValue('error', errorStr) then // Check if an error was returned 221 | begin 222 | errorStr := 'Error sending Email (generic).'; // Default error 223 | errJSONValue := errJSON_Obj.ParseJSONValue(restRequest.Response.Content, false, true); 224 | 225 | if errJSONValue <> nil then errJSONValue := (errJSONValue as TJSONObject).Get('error').JSONValue; 226 | if errJSONValue <> nil then errorStr := (errJSONValue as TJSONObject).GetValue('message').Value; 227 | 228 | raise Exception.Create('Google: ' + errorStr); 229 | end; 230 | 231 | finally 232 | restClient.Free; 233 | msgStream.Free; 234 | errJSON_Obj.Free; 235 | errJSONValue.Free; 236 | end; 237 | end; 238 | 239 | 240 | { ***** / / ***** / / ****** / / ***** } 241 | function TFRM_sendMail.getEmailMessage_fromForm: tIdMessage; 242 | var 243 | i: integer; 244 | myIndyMsg: TIdMessage; 245 | textPart: TIdText; 246 | htmlPart: TIdText; 247 | htmlStr: string; 248 | begin 249 | myIndyMsg := TIdMessage.Create; 250 | 251 | //************ 252 | // Message cfg 253 | myIndyMsg.clear; 254 | myIndyMsg.Encoding := meMIME; 255 | myIndyMsg.BccList.EMailAddresses := EDT_toEmail.Text; 256 | myIndyMsg.from.Name := 'Delphi Application'; 257 | myIndyMsg.from.Address := EDT_email_google.Text; 258 | myIndyMsg.CharSet := 'UTF-8'; 259 | myIndyMsg.Subject := EDT_toSubject.Text; 260 | 261 | // Only 1 recipient, use To field instead of Bcc 262 | if myIndyMsg.BccList.Count = 1 then 263 | begin 264 | myIndyMsg.Recipients.Add.Assign(myIndyMsg.BccList.Items[0]); 265 | myIndyMsg.BccList.Delete(0); 266 | end; 267 | 268 | myIndyMsg.Body.Clear; 269 | myIndyMsg.ContentType := 'multipart/alternative'; 270 | 271 | // Plain version 272 | textPart := TIdText.Create(myIndyMsg.MessageParts); 273 | textPart.Body.Text := MEM_toMessage.Lines.Text; 274 | textPart.ContentType := 'text/plain'; 275 | textPart.CharSet := 'UTF-8'; 276 | textPart.ParentPart := -1; 277 | 278 | // HTML version 279 | htmlStr := ''; 280 | for i := 0 to MEM_toMessage.Lines.Count - 1 do 281 | htmlStr := htmlStr + MEM_toMessage.Lines[i] + '
'; 282 | htmlStr := '
' + htmlStr + '
'; 283 | 284 | htmlPart := TIdText.Create(myIndyMsg.MessageParts); 285 | htmlPart.ContentType := 'text/html'; 286 | htmlPart.CharSet := 'UTF-8'; 287 | htmlPart.ParentPart := -1; 288 | htmlPart.Body.Text := htmlStr; 289 | 290 | result := myIndyMsg; 291 | end; 292 | 293 | end. 294 | --------------------------------------------------------------------------------