1/* 2 * Copyright © 2020 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[ModuleInit] 9public void peas_register_types(TypeModule module) { 10 Peas.ObjectModule obj = module as Peas.ObjectModule; 11 obj.register_extension_type( 12 typeof(Plugin.PluginBase), 13 typeof(Plugin.EmailTemplates) 14 ); 15} 16 17/** 18 * Enables editing and sending email templates. 19 */ 20public class Plugin.EmailTemplates : 21 PluginBase, FolderExtension, EmailExtension { 22 23 24 // Translators: Templates folder name alternatives. Separate names 25 // using a vertical bar and put the most common localized name to 26 // the front for the default. English names do not need to be 27 // included. 28 private const string LOC_NAMES = _( 29 "Templates | Template Mail | Template Email | Template E-Mail" 30 ); 31 // This must be identical to he above except without translation 32 private const string UNLOC_NAMES = ( 33 "Templates | Template Mail | Template Email | Template E-Mail" 34 ); 35 36 37 private const string ACTION_NEW = "new-template"; 38 private const string ACTION_EDIT = "edit-template"; 39 private const string ACTION_SEND = "send-template"; 40 41 private const int INFO_BAR_PRIORITY = 0; 42 43 44 public FolderContext folders { 45 get; set construct; 46 } 47 48 public EmailContext email { 49 get; set construct; 50 } 51 52 53 private FolderStore? folder_store = null; 54 private EmailStore? email_store = null; 55 56 private GLib.SimpleAction? new_action = null; 57 private GLib.SimpleAction? edit_action = null; 58 private GLib.SimpleAction? send_action = null; 59 60 private Gee.Map<Folder,InfoBar> info_bars = 61 new Gee.HashMap<Folder,InfoBar>(); 62 63 private Gee.List<string> folder_names = new Gee.ArrayList<string>(); 64 65 private GLib.Cancellable cancellable = new GLib.Cancellable(); 66 67 68 public override async void activate(bool is_startup) throws GLib.Error { 69 // Add localised first, so if we need to create a folder it 70 // will be created localised. 71 Geary.iterate_array(LOC_NAMES.split("|")).map<string>( 72 (name) => name.strip() 73 ).add_all_to(this.folder_names); 74 Geary.iterate_array(UNLOC_NAMES.split("|")).map<string>( 75 (name) => name.strip() 76 ).add_all_to(this.folder_names); 77 78 this.folder_store = yield this.folders.get_folder_store(); 79 this.folder_store.folders_available.connect(on_folders_available); 80 this.folder_store.folders_unavailable.connect(on_folders_unavailable); 81 this.folder_store.folders_type_changed.connect(on_folders_type_changed); 82 this.folder_store.folder_selected.connect(on_folder_selected); 83 84 this.email_store = yield this.email.get_email_store(); 85 this.email_store.email_displayed.connect(on_email_displayed); 86 87 this.new_action = new GLib.SimpleAction( 88 ACTION_NEW, this.folder_store.folder_variant_type 89 ); 90 this.new_action.activate.connect(on_new_activated); 91 this.plugin_application.register_action(this.new_action); 92 93 this.edit_action = new GLib.SimpleAction( 94 ACTION_EDIT, this.email_store.email_identifier_variant_type 95 ); 96 this.edit_action.activate.connect(on_edit_activated); 97 this.plugin_application.register_action(this.edit_action); 98 99 this.send_action = new GLib.SimpleAction( 100 ACTION_SEND, this.email_store.email_identifier_variant_type 101 ); 102 this.send_action.activate.connect(on_send_activated); 103 this.plugin_application.register_action(this.send_action); 104 105 add_folders(this.folder_store.get_folders()); 106 } 107 108 public override async void deactivate(bool is_shutdown) throws GLib.Error { 109 this.cancellable.cancel(); 110 111 // Take a copy of the keys so the collection doesn't asplode 112 // as it is being modified. 113 foreach (var folder in this.info_bars.keys.to_array()) { 114 unregister_folder(folder); 115 } 116 this.info_bars.clear(); 117 this.folder_names.clear(); 118 119 this.plugin_application.deregister_action(this.new_action); 120 this.new_action = null; 121 122 this.plugin_application.deregister_action(this.edit_action); 123 this.edit_action = null; 124 125 this.plugin_application.deregister_action(this.send_action); 126 this.send_action = null; 127 128 this.folder_store.folders_available.disconnect(on_folders_available); 129 this.folder_store.folders_unavailable.disconnect(on_folders_unavailable); 130 this.folder_store.folders_type_changed.disconnect(on_folders_type_changed); 131 this.folder_store.folder_selected.disconnect(on_folder_selected); 132 this.folder_store = null; 133 134 this.email_store.email_displayed.disconnect(on_email_displayed); 135 this.email_store = null; 136 } 137 138 private async void edit_email(Folder? target, EmailIdentifier? id, bool send) { 139 var account = (target != null) ? target.account : id.account; 140 try { 141 Plugin.Composer? composer = null; 142 if (id != null) { 143 composer = yield this.plugin_application.compose_with_context( 144 id.account, 145 Composer.ContextType.EDIT, 146 id 147 ); 148 } else { 149 composer = yield this.plugin_application.compose_blank(account); 150 } 151 if (!send) { 152 var folder = target; 153 if (folder == null && id != null) { 154 var containing = yield this.folder_store.list_containing_folders( 155 id, this.cancellable 156 ); 157 folder = containing.first_match( 158 (f) => this.info_bars.has_key(f) 159 ); 160 } 161 composer.save_to_folder(folder); 162 composer.can_send = false; 163 } 164 165 composer.present(); 166 } catch (GLib.Error err) { 167 warning("Unable to construct composer: %s", err.message); 168 } 169 } 170 171 private void add_folders(Gee.Collection<Folder> to_add) { 172 Folder? inbox = null; 173 var found_templates = false; 174 foreach (var folder in to_add) { 175 if (folder.used_as == INBOX) { 176 inbox = folder; 177 } else if (folder.display_name in this.folder_names) { 178 register_folder(folder); 179 found_templates = true; 180 } 181 } 182 183 // XXX There is no way at the moment to determine when all 184 // local folders have been loaded, but since they are all done 185 // in once batch, it's a safe bet that if we've seen the 186 // Inbox, then the local folder set should contain a templates 187 // folder, if one is available. If there isn't, we need to 188 // create it. 189 if (!found_templates && inbox != null) { 190 debug("Creating templates folder"); 191 this.create_folder.begin(inbox.account); 192 } 193 } 194 195 private void register_folder(Folder target) { 196 try { 197 this.folders.register_folder_used_as( 198 target, 199 // Translators: The name of the folder used to 200 // store email templates 201 _("Templates"), 202 "folder-templates-symbolic" 203 ); 204 this.info_bars.set( 205 target, 206 new_templates_folder_info_bar(target) 207 ); 208 } catch (GLib.Error err) { 209 warning( 210 "Failed to register %s as templates folder: %s", 211 target.persistent_id, 212 err.message 213 ); 214 } 215 } 216 217 private void unregister_folder(Folder target) { 218 var info_bar = this.info_bars.get(target); 219 if (info_bar != null) { 220 try { 221 this.folders.unregister_folder_used_as(target); 222 } catch (GLib.Error err) { 223 warning( 224 "Failed to unregister %s as templates folder: %s", 225 target.persistent_id, 226 err.message 227 ); 228 } 229 this.folders.remove_folder_info_bar(target, info_bar); 230 this.info_bars.unset(target); 231 } 232 } 233 234 private async void create_folder(Account account) { 235 try { 236 yield this.folder_store.create_personal_folder( 237 account, 238 this.folder_names[0], 239 this.cancellable 240 ); 241 // Don't need to explicitly register the folder here, it 242 // will get picked up via the available signal 243 } catch (GLib.Error err) { 244 warning("Failed to create templates folder: %s", err.message); 245 } 246 } 247 248 private void update_folder(Folder target) { 249 var info_bar = this.info_bars.get(target); 250 if (info_bar != null) { 251 this.folders.add_folder_info_bar( 252 target, info_bar, INFO_BAR_PRIORITY 253 ); 254 } 255 } 256 257 private async void update_email(Email target) { 258 var containing = Gee.Collection.empty<Folder>(); 259 try { 260 containing = yield this.folder_store.list_containing_folders( 261 target.identifier, this.cancellable 262 ); 263 } catch (GLib.Error err) { 264 warning("Could not load folders for email: %s", err.message); 265 } 266 if (containing.any_match((f) => this.info_bars.has_key(f))) { 267 this.email.add_email_info_bar( 268 target.identifier, 269 new_template_email_info_bar(target.identifier), 270 INFO_BAR_PRIORITY 271 ); 272 } 273 } 274 275 private InfoBar new_templates_folder_info_bar(Folder target) { 276 var bar = this.info_bars.get(target); 277 if (bar == null) { 278 bar = new InfoBar(target.display_name); 279 bar.primary_button = new Actionable( 280 // Translators: Info bar button label for creating a 281 // new email template 282 _("New"), 283 this.new_action, 284 target.to_variant() 285 ); 286 this.info_bars.set(target, bar); 287 } 288 return bar; 289 } 290 291 private InfoBar new_template_email_info_bar(EmailIdentifier target) { 292 // Translators: Infobar status label for an email template 293 var bar = new InfoBar(_("Message template")); 294 bar.primary_button = new Actionable( 295 // Translators: Info bar button label for sending an 296 // email template 297 _("Send"), 298 this.send_action, 299 target.to_variant() 300 ); 301 bar.secondary_buttons.add( 302 new Actionable( 303 // Translators: Info bar button label for editing an 304 // existing email template 305 _("Edit"), 306 this.edit_action, 307 target.to_variant() 308 ) 309 ); 310 return bar; 311 } 312 313 private void on_folders_available(Gee.Collection<Folder> available) { 314 add_folders(available); 315 } 316 317 private void on_folders_unavailable(Gee.Collection<Folder> unavailable) { 318 foreach (var folder in unavailable) { 319 unregister_folder(folder); 320 } 321 } 322 323 private void on_folders_type_changed(Gee.Collection<Folder> changed) { 324 foreach (var folder in changed) { 325 unregister_folder(folder); 326 if (folder.display_name in this.folder_names) { 327 register_folder(folder); 328 } 329 update_folder(folder); 330 } 331 } 332 333 private void on_folder_selected(Folder selected) { 334 update_folder(selected); 335 } 336 337 private void on_new_activated(GLib.Action action, GLib.Variant? target) { 338 if (this.folder_store != null && target != null) { 339 Folder? folder = this.folder_store.get_folder_for_variant(target); 340 if (folder != null) { 341 this.edit_email.begin(folder, null, false); 342 } 343 } 344 } 345 346 private void on_edit_activated(GLib.Action action, GLib.Variant? target) { 347 if (this.email_store != null && target != null) { 348 EmailIdentifier? id = 349 this.email_store.get_email_identifier_for_variant(target); 350 if (id != null) { 351 this.edit_email.begin(null, id, false); 352 } 353 } 354 } 355 356 private void on_send_activated(GLib.Action action, GLib.Variant? target) { 357 if (this.email_store != null && target != null) { 358 EmailIdentifier? id = 359 this.email_store.get_email_identifier_for_variant(target); 360 if (id != null) { 361 this.edit_email.begin(null, id, true); 362 } 363 } 364 } 365 366 private void on_email_displayed(Email email) { 367 this.update_email.begin(email); 368 } 369 370} 371