1/* 2 * Copyright 2018-2019 Michael Gratton <mike@vee.net> 3 * 4 * This software is licensed under the GNU Lesser General Public License 5 * (version 2.1 or later). See the COPYING file in this distribution. 6 */ 7 8/** 9 * An account editor pane for adding a new account. 10 */ 11[GtkTemplate (ui = "/org/gnome/Geary/accounts_editor_add_pane.ui")] 12internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane { 13 14 15 internal Gtk.Widget initial_widget { 16 get { return this.real_name.value; } 17 } 18 19 /** {@inheritDoc} */ 20 internal bool is_operation_running { 21 get { return !this.sensitive; } 22 protected set { update_operation_ui(value); } 23 } 24 25 /** {@inheritDoc} */ 26 internal GLib.Cancellable? op_cancellable { 27 get; protected set; default = new GLib.Cancellable(); 28 } 29 30 protected weak Accounts.Editor editor { get; set; } 31 32 private Geary.ServiceProvider provider; 33 34 private Manager accounts; 35 private Geary.Engine engine; 36 37 38 [GtkChild] private unowned Gtk.HeaderBar header; 39 40 [GtkChild] private unowned Gtk.Grid pane_content; 41 42 [GtkChild] private unowned Gtk.Adjustment pane_adjustment; 43 44 [GtkChild] private unowned Gtk.ListBox details_list; 45 46 [GtkChild] private unowned Gtk.Grid receiving_panel; 47 48 [GtkChild] private unowned Gtk.ListBox receiving_list; 49 50 [GtkChild] private unowned Gtk.Grid sending_panel; 51 52 [GtkChild] private unowned Gtk.ListBox sending_list; 53 54 [GtkChild] private unowned Gtk.Button create_button; 55 56 [GtkChild] private unowned Gtk.Button back_button; 57 58 [GtkChild] private unowned Gtk.Spinner create_spinner; 59 60 private NameRow real_name; 61 private EmailRow email = new EmailRow(); 62 private string last_valid_email = ""; 63 private string last_valid_hostname = ""; 64 65 private HostnameRow imap_hostname = new HostnameRow(Geary.Protocol.IMAP); 66 private TransportSecurityRow imap_tls = new TransportSecurityRow(); 67 private LoginRow imap_login = new LoginRow(); 68 private PasswordRow imap_password = new PasswordRow(); 69 70 private HostnameRow smtp_hostname = new HostnameRow(Geary.Protocol.SMTP); 71 private TransportSecurityRow smtp_tls = new TransportSecurityRow(); 72 private OutgoingAuthRow smtp_auth = new OutgoingAuthRow(); 73 private LoginRow smtp_login = new LoginRow(); 74 private PasswordRow smtp_password = new PasswordRow(); 75 76 private bool controls_valid = false; 77 78 79 internal EditorAddPane(Editor editor, Geary.ServiceProvider provider) { 80 this.editor = editor; 81 this.provider = provider; 82 83 this.accounts = editor.application.controller.account_manager; 84 this.engine = editor.application.engine; 85 86 this.pane_content.set_focus_vadjustment(this.pane_adjustment); 87 88 this.details_list.set_header_func(Editor.seperator_headers); 89 this.receiving_list.set_header_func(Editor.seperator_headers); 90 this.sending_list.set_header_func(Editor.seperator_headers); 91 92 if (provider != Geary.ServiceProvider.OTHER) { 93 this.details_list.add( 94 new ServiceProviderRow<EditorAddPane>( 95 provider, 96 // Translators: Label for adding an email account 97 // account for a generic IMAP service provider. 98 _("All others") 99 ) 100 ); 101 this.receiving_panel.hide(); 102 this.sending_panel.hide(); 103 } 104 105 this.real_name = new NameRow(this.accounts.get_account_name()); 106 107 this.details_list.add(this.real_name); 108 this.details_list.add(this.email); 109 110 this.real_name.validator.state_changed.connect(on_validated); 111 this.real_name.value.activate.connect(on_activated); 112 this.email.validator.state_changed.connect(on_validated); 113 this.email.value.activate.connect(on_activated); 114 this.email.value.changed.connect(on_email_changed); 115 116 this.imap_hostname.validator.state_changed.connect(on_validated); 117 this.imap_hostname.value.activate.connect(on_activated); 118 this.imap_tls.hide(); 119 this.imap_login.validator.state_changed.connect(on_validated); 120 this.imap_login.value.activate.connect(on_activated); 121 this.imap_password.validator.state_changed.connect(on_validated); 122 this.imap_password.value.activate.connect(on_activated); 123 124 this.smtp_hostname.validator.state_changed.connect(on_validated); 125 this.smtp_hostname.value.activate.connect(on_activated); 126 this.smtp_tls.hide(); 127 this.smtp_auth.value.changed.connect(on_smtp_auth_changed); 128 this.smtp_login.validator.state_changed.connect(on_validated); 129 this.smtp_login.value.activate.connect(on_activated); 130 this.smtp_password.validator.state_changed.connect(on_validated); 131 this.smtp_password.value.activate.connect(on_activated); 132 133 if (provider == Geary.ServiceProvider.OTHER) { 134 this.receiving_list.add(this.imap_hostname); 135 this.receiving_list.add(this.imap_tls); 136 this.receiving_list.add(this.imap_login); 137 this.receiving_list.add(this.imap_password); 138 139 this.sending_list.add(this.smtp_hostname); 140 this.sending_list.add(this.smtp_tls); 141 this.sending_list.add(this.smtp_auth); 142 } else { 143 this.details_list.add(this.imap_password); 144 } 145 } 146 147 internal Gtk.HeaderBar get_header() { 148 return this.header; 149 } 150 151 private async void validate_account(GLib.Cancellable? cancellable) { 152 this.is_operation_running = true; 153 154 bool is_valid = false; 155 string? message = null; 156 Gtk.Widget? to_focus = null; 157 158 Geary.AccountInformation account = 159 yield this.accounts.new_orphan_account( 160 this.provider, 161 new Geary.RFC822.MailboxAddress( 162 this.real_name.value.text.strip(), 163 this.email.value.text.strip() 164 ), 165 cancellable 166 ); 167 168 account.incoming = new_imap_service(); 169 account.outgoing = new_smtp_service(); 170 account.untrusted_host.connect(on_untrusted_host); 171 172 if (this.provider == Geary.ServiceProvider.OTHER) { 173 bool imap_valid = false; 174 bool smtp_valid = false; 175 176 try { 177 yield this.engine.validate_imap( 178 account, account.incoming, cancellable 179 ); 180 imap_valid = true; 181 } catch (Geary.ImapError.UNAUTHENTICATED err) { 182 debug("Error authenticating IMAP service: %s", err.message); 183 to_focus = this.imap_login.value; 184 // Translators: In-app notification label 185 message = _("Check your receiving login and password"); 186 } catch (GLib.TlsError.BAD_CERTIFICATE err) { 187 debug("Error validating IMAP certificate: %s", err.message); 188 // Nothing to do here, since the untrusted host 189 // handler will be dealing with it 190 } catch (GLib.IOError.CANCELLED err) { 191 // Nothing to do here, someone just cancelled 192 debug("IMAP validation was cancelled: %s", err.message); 193 } catch (GLib.Error err) { 194 Geary.ErrorContext context = new Geary.ErrorContext(err); 195 debug("Error validating IMAP service: %s", 196 context.format_full_error()); 197 this.imap_tls.show(); 198 to_focus = this.imap_hostname.value; 199 // Translators: In-app notification label 200 message = _("Check your receiving server details"); 201 } 202 203 if (imap_valid) { 204 debug("Validating SMTP..."); 205 try { 206 yield this.engine.validate_smtp( 207 account, 208 account.outgoing, 209 account.incoming.credentials, 210 cancellable 211 ); 212 smtp_valid = true; 213 } catch (Geary.SmtpError.AUTHENTICATION_FAILED err) { 214 debug("Error authenticating SMTP service: %s", err.message); 215 // There was an SMTP auth error, but IMAP already 216 // succeeded, so the user probably needs to 217 // specify custom creds here 218 this.smtp_auth.value.source = 219 Geary.Credentials.Requirement.CUSTOM; 220 to_focus = this.smtp_login.value; 221 // Translators: In-app notification label 222 message = _("Check your sending login and password"); 223 } catch (GLib.TlsError.BAD_CERTIFICATE err) { 224 // Nothing to do here, since the untrusted host 225 // handler will be dealing with it 226 } catch (GLib.IOError.CANCELLED err) { 227 // Nothing to do here, someone just cancelled 228 debug("SMTP validation was cancelled: %s", err.message); 229 } catch (GLib.Error err) { 230 Geary.ErrorContext context = new Geary.ErrorContext(err); 231 debug("Error validating SMTP service: %s", 232 context.format_full_error()); 233 this.smtp_tls.show(); 234 to_focus = this.smtp_hostname.value; 235 // Translators: In-app notification label 236 message = _("Check your sending server details"); 237 } 238 } 239 240 is_valid = imap_valid && smtp_valid; 241 } else { 242 try { 243 yield this.engine.validate_imap( 244 account, account.incoming, cancellable 245 ); 246 is_valid = true; 247 } catch (Geary.ImapError.UNAUTHENTICATED err) { 248 debug("Error authenticating provider: %s", err.message); 249 to_focus = this.email.value; 250 // Translators: In-app notification label 251 message = _("Check your email address and password"); 252 } catch (GLib.TlsError.BAD_CERTIFICATE err) { 253 // Nothing to do here, since the untrusted host 254 // handler will be dealing with it 255 debug("Error validating SMTP certificate: %s", err.message); 256 } catch (GLib.Error err) { 257 Geary.ErrorContext context = new Geary.ErrorContext(err); 258 debug("Error validating SMTP service: %s", 259 context.format_full_error()); 260 is_valid = false; 261 // Translators: In-app notification label 262 message = _("Could not connect, check your network"); 263 } 264 } 265 266 if (is_valid) { 267 try { 268 yield this.accounts.create_account(account, cancellable); 269 this.editor.pop(); 270 } catch (GLib.Error err) { 271 debug("Failed to create new local account: %s", err.message); 272 is_valid = false; 273 // Translators: In-app notification label for a 274 // generic error creating an account 275 message = _("An unexpected problem occurred"); 276 } 277 } 278 279 account.untrusted_host.disconnect(on_untrusted_host); 280 this.is_operation_running = false; 281 282 // Focus and pop up the notification after re-sensitising 283 // so it actually succeeds. 284 if (!is_valid) { 285 if (to_focus != null) { 286 to_focus.grab_focus(); 287 } 288 if (message != null) { 289 this.editor.add_notification( 290 new Components.InAppNotification( 291 // Translators: In-app notification label, the 292 // string substitution is a more detailed reason. 293 _("Account not created: %s").printf(message) 294 ) 295 ); 296 } 297 } 298 } 299 300 private Geary.ServiceInformation new_imap_service() { 301 Geary.ServiceInformation service = new Geary.ServiceInformation( 302 Geary.Protocol.IMAP, this.provider 303 ); 304 305 if (this.provider == Geary.ServiceProvider.OTHER) { 306 service.credentials = new Geary.Credentials( 307 Geary.Credentials.Method.PASSWORD, 308 this.imap_login.value.get_text().strip(), 309 this.imap_password.value.get_text().strip() 310 ); 311 312 Components.NetworkAddressValidator host = 313 (Components.NetworkAddressValidator) 314 this.imap_hostname.validator; 315 GLib.NetworkAddress address = host.validated_address; 316 service.host = address.hostname; 317 service.port = (uint16) address.port; 318 service.transport_security = this.imap_tls.value.method; 319 320 if (service.port == 0) { 321 service.port = service.get_default_port(); 322 } 323 } else { 324 service.credentials = new Geary.Credentials( 325 Geary.Credentials.Method.PASSWORD, 326 this.email.value.get_text().strip(), 327 this.imap_password.value.get_text().strip() 328 ); 329 } 330 331 return service; 332 } 333 334 private Geary.ServiceInformation new_smtp_service() { 335 Geary.ServiceInformation service = new Geary.ServiceInformation( 336 Geary.Protocol.SMTP, this.provider 337 ); 338 339 if (this.provider == Geary.ServiceProvider.OTHER) { 340 service.credentials_requirement = this.smtp_auth.value.source; 341 if (service.credentials_requirement == 342 Geary.Credentials.Requirement.CUSTOM) { 343 service.credentials = new Geary.Credentials( 344 Geary.Credentials.Method.PASSWORD, 345 this.smtp_login.value.get_text().strip(), 346 this.smtp_password.value.get_text().strip() 347 ); 348 } 349 350 Components.NetworkAddressValidator host = 351 (Components.NetworkAddressValidator) 352 this.smtp_hostname.validator; 353 GLib.NetworkAddress address = host.validated_address; 354 355 service.host = address.hostname; 356 service.port = (uint16) address.port; 357 service.transport_security = this.smtp_tls.value.method; 358 359 if (service.port == 0) { 360 service.port = service.get_default_port(); 361 } 362 } 363 364 return service; 365 } 366 367 private void check_validation() { 368 bool controls_valid = true; 369 foreach (Gtk.ListBox list in new Gtk.ListBox[] { 370 this.details_list, this.receiving_list, this.sending_list 371 }) { 372 list.foreach((child) => { 373 AddPaneRow? validatable = child as AddPaneRow; 374 if (validatable != null && !validatable.validator.is_valid) { 375 controls_valid = false; 376 } 377 }); 378 } 379 this.create_button.set_sensitive(controls_valid); 380 this.controls_valid = controls_valid; 381 } 382 383 private void update_operation_ui(bool is_running) { 384 this.create_spinner.visible = is_running; 385 this.create_spinner.active = is_running; 386 this.create_button.sensitive = !is_running; 387 this.back_button.sensitive = !is_running; 388 this.sensitive = !is_running; 389 } 390 391 private void on_validated(Components.Validator.Trigger reason) { 392 check_validation(); 393 if (this.controls_valid && reason == Components.Validator.Trigger.ACTIVATED) { 394 this.create_button.clicked(); 395 } 396 } 397 398 private void on_activated() { 399 if (this.controls_valid) { 400 this.create_button.clicked(); 401 } 402 } 403 404 private void on_email_changed() { 405 Gtk.Entry imap_login_entry = this.imap_login.value; 406 Gtk.Entry smtp_login_entry = this.smtp_login.value; 407 Gtk.Entry imap_hostname_entry = this.imap_hostname.value; 408 Gtk.Entry smtp_hostname_entry = this.smtp_hostname.value; 409 string email = ""; 410 string hostname = ""; 411 string imap_hostname = ""; 412 string smtp_hostname = ""; 413 string last_imap_hostname = ""; 414 string last_smtp_hostname = ""; 415 416 if (this.email.validator.state == Components.Validator.Validity.VALID) { 417 email = this.email.value.text; 418 hostname = email.split("@")[1]; 419 smtp_hostname = "smtp." + hostname; 420 imap_hostname = "imap." + hostname; 421 } 422 423 if (imap_login_entry.text == this.last_valid_email) { 424 imap_login_entry.text = email; 425 } 426 if (smtp_login_entry.text == this.last_valid_email) { 427 smtp_login_entry.text = email; 428 } 429 430 if (this.last_valid_hostname != "") { 431 last_imap_hostname = "imap." + this.last_valid_hostname; 432 last_smtp_hostname = "smtp." + this.last_valid_hostname; 433 } 434 if (imap_hostname_entry.text == last_imap_hostname) { 435 imap_hostname_entry.text = imap_hostname; 436 } 437 if (smtp_hostname_entry.text == last_smtp_hostname) { 438 smtp_hostname_entry.text = smtp_hostname; 439 } 440 441 this.last_valid_email = email; 442 this.last_valid_hostname = hostname; 443 } 444 445 private void on_smtp_auth_changed() { 446 if (this.smtp_auth.value.source == Geary.Credentials.Requirement.CUSTOM) { 447 this.sending_list.add(this.smtp_login); 448 this.sending_list.add(this.smtp_password); 449 } else if (this.smtp_login.parent != null) { 450 this.sending_list.remove(this.smtp_login); 451 this.sending_list.remove(this.smtp_password); 452 } 453 check_validation(); 454 } 455 456 private void on_untrusted_host(Geary.AccountInformation account, 457 Geary.ServiceInformation service, 458 Geary.Endpoint endpoint, 459 GLib.TlsConnection cx) { 460 this.editor.prompt_pin_certificate.begin( 461 account, service, endpoint, this.op_cancellable, 462 (obj, res) => { 463 try { 464 this.editor.prompt_pin_certificate.end(res); 465 } catch (Application.CertificateManagerError err) { 466 // All good, just drop back into the editor 467 // window. 468 return; 469 } 470 471 // Kick off another attempt to validate 472 this.validate_account.begin(this.op_cancellable); 473 }); 474 } 475 476 [GtkCallback] 477 private void on_create_button_clicked() { 478 this.validate_account.begin(this.op_cancellable); 479 } 480 481 [GtkCallback] 482 private void on_back_button_clicked() { 483 this.editor.pop(); 484 } 485 486 [GtkCallback] 487 private bool on_list_keynav_failed(Gtk.Widget widget, 488 Gtk.DirectionType direction) { 489 bool ret = Gdk.EVENT_PROPAGATE; 490 Gtk.Container? next = null; 491 if (direction == Gtk.DirectionType.DOWN) { 492 if (widget == this.details_list) { 493 debug("Have details!"); 494 next = this.receiving_list; 495 } else if (widget == this.receiving_list) { 496 next = this.sending_list; 497 } 498 } else if (direction == Gtk.DirectionType.UP) { 499 if (widget == this.sending_list) { 500 next = this.receiving_list; 501 } else if (widget == this.receiving_list) { 502 next = this.details_list; 503 } 504 } 505 506 if (next != null) { 507 next.child_focus(direction); 508 ret = Gdk.EVENT_STOP; 509 } 510 return ret; 511 } 512 513} 514 515 516private abstract class Accounts.AddPaneRow<Value> : 517 LabelledEditorRow<EditorAddPane,Value> { 518 519 520 internal Components.Validator? validator { get; protected set; } 521 522 523 protected AddPaneRow(string label, Value value) { 524 base(label, value); 525 this.activatable = false; 526 } 527 528} 529 530 531private abstract class Accounts.EntryRow : AddPaneRow<Gtk.Entry> { 532 533 534 private Components.EntryUndo undo; 535 536 537 protected EntryRow(string label, 538 string? initial_value = null, 539 string? placeholder = null) { 540 base(label, new Gtk.Entry()); 541 542 this.value.text = initial_value ?? ""; 543 this.value.placeholder_text = placeholder ?? ""; 544 this.value.width_chars = 16; 545 546 this.undo = new Components.EntryUndo(this.value); 547 } 548 549 public override bool focus(Gtk.DirectionType direction) { 550 bool ret = Gdk.EVENT_PROPAGATE; 551 switch (direction) { 552 case Gtk.DirectionType.TAB_FORWARD: 553 case Gtk.DirectionType.TAB_BACKWARD: 554 ret = this.value.child_focus(direction); 555 break; 556 557 default: 558 ret = base.focus(direction); 559 break; 560 } 561 562 return ret; 563 } 564 565} 566 567 568private class Accounts.NameRow : EntryRow { 569 570 public NameRow(string default_name) { 571 // Translators: Label for the person's actual name when adding 572 // an account 573 base(_("Your name"), default_name.strip()); 574 this.validator = new Components.Validator(this.value); 575 if (this.value.text != "") { 576 // Validate if the string is non-empty so it will be good 577 // to go from the start 578 this.validator.validate(); 579 } 580 } 581 582} 583 584 585private class Accounts.EmailRow : EntryRow { 586 587 588 public EmailRow() { 589 base( 590 _("Email address"), 591 null, 592 // Translators: Placeholder for the default sender address 593 // when adding an account 594 _("person@example.com") 595 ); 596 this.value.input_purpose = Gtk.InputPurpose.EMAIL; 597 this.validator = new Components.EmailValidator(this.value); 598 } 599 600} 601 602 603private class Accounts.LoginRow : EntryRow { 604 605 public LoginRow() { 606 // Translators: Label for an IMAP/SMTP service login/user name 607 // when adding an account 608 base(_("Login name")); 609 // Logins are not infrequently the same as the user's email 610 // address 611 this.value.input_purpose = Gtk.InputPurpose.EMAIL; 612 this.validator = new Components.Validator(this.value); 613 } 614 615} 616 617 618private class Accounts.PasswordRow : EntryRow { 619 620 621 public PasswordRow() { 622 base(_("Password")); 623 this.value.visibility = false; 624 this.value.input_purpose = Gtk.InputPurpose.PASSWORD; 625 this.validator = new Components.Validator(this.value); 626 } 627 628} 629 630 631private class Accounts.HostnameRow : EntryRow { 632 633 634 private Geary.Protocol type; 635 636 637 public HostnameRow(Geary.Protocol type) { 638 string label = ""; 639 string placeholder = ""; 640 switch (type) { 641 case Geary.Protocol.IMAP: 642 // Translators: Label for the IMAP server hostname when 643 // adding an account. 644 label = _("IMAP server"); 645 // Translators: Placeholder for the IMAP server hostname 646 // when adding an account. 647 placeholder = _("imap.example.com"); 648 break; 649 650 case Geary.Protocol.SMTP: 651 // Translators: Label for the SMTP server hostname when 652 // adding an account. 653 label = _("SMTP server"); 654 // Translators: Placeholder for the SMTP server hostname 655 // when adding an account. 656 placeholder = _("smtp.example.com"); 657 break; 658 } 659 660 base(label, null, placeholder); 661 this.type = type; 662 663 this.validator = new Components.NetworkAddressValidator(this.value, 0); 664 } 665 666} 667 668 669private class Accounts.TransportSecurityRow : 670 LabelledEditorRow<EditorAddPane,TlsComboBox> { 671 672 public TransportSecurityRow() { 673 TlsComboBox value = new TlsComboBox(); 674 base(value.label, value); 675 // Set to Transport TLS by default per RFC 8314 676 this.value.method = Geary.TlsNegotiationMethod.TRANSPORT; 677 } 678 679} 680 681 682private class Accounts.OutgoingAuthRow : 683 LabelledEditorRow<EditorAddPane,OutgoingAuthComboBox> { 684 685 public OutgoingAuthRow() { 686 OutgoingAuthComboBox value = new OutgoingAuthComboBox(); 687 base(value.label, value); 688 689 this.activatable = false; 690 this.value.source = Geary.Credentials.Requirement.USE_INCOMING; 691 } 692 693} 694