View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2009  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package davmail;
20  
21  import davmail.ui.tray.DavGatewayTray;
22  import org.apache.log4j.ConsoleAppender;
23  import org.apache.log4j.FileAppender;
24  import org.apache.log4j.Level;
25  import org.apache.log4j.Logger;
26  import org.apache.log4j.PatternLayout;
27  import org.apache.log4j.RollingFileAppender;
28  
29  import java.io.BufferedReader;
30  import java.io.BufferedWriter;
31  import java.io.File;
32  import java.io.FileInputStream;
33  import java.io.FileOutputStream;
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.io.InputStreamReader;
37  import java.io.OutputStreamWriter;
38  import java.nio.charset.StandardCharsets;
39  import java.nio.file.Files;
40  import java.nio.file.Path;
41  import java.nio.file.Paths;
42  import java.nio.file.attribute.FileAttribute;
43  import java.nio.file.attribute.PosixFilePermissions;
44  import java.util.ArrayList;
45  import java.util.Enumeration;
46  import java.util.Iterator;
47  import java.util.Map;
48  import java.util.Properties;
49  import java.util.TreeSet;
50  
51  import static org.apache.http.util.TextUtils.isEmpty;
52  
53  /**
54   * Settings facade.
55   * DavMail settings are stored in the .davmail.properties file in current
56   * user home directory or in the file specified on the command line.
57   */
58  public final class Settings {
59  
60      private static final Logger LOGGER = Logger.getLogger(Settings.class);
61  
62      public static final String OUTLOOK_URL = "https://outlook.office365.com";
63      public static final String O365_URL = OUTLOOK_URL + "/EWS/Exchange.asmx";
64  
65      public static final String GRAPH_URL = "https://graph.microsoft.com";
66  
67      public static final String O365_LOGIN_URL = "https://login.microsoftonline.com";
68  
69      // protocol modes
70      public static final String O365_EWS = "O365EWS"; // classic O365 over EWS
71      public static final String O365_GRAPH = "O365Graph"; // new O365 graph backend
72      public static final String EXCHANGE_EWS = "ExchangeEWS"; // on prem Exchange
73      public static final String EXCHANGE_WEBDAV = "ExchangeWebDav"; // on prem Exchange legacy webdav
74  
75      // authentication modes
76      public static final String O365_INTERACTIVE = "O365Interactive"; // interactive embedded browser O365 login
77      public static final String O365_MANUAL = "O365Manual"; // manual O365 authentication dialog
78      public static final String O365_DEVICECODE = "O365DeviceCode"; // O365 device code authentication
79      public static final String O365_TRANSPARENT = "O365Transparent"; // Transparent automated authentication
80  
81      // legacy protocol modes
82      public static final String O365_MODERN = "O365Modern";
83      public static final String O365 = "O365";
84      public static final String WEBDAV = "WebDav";
85      public static final String EWS = "EWS";
86      public static final String AUTO = "Auto";
87  
88      public static final String EDGE_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 Edg/90.0.818.49";
89  
90      private Settings() {
91      }
92  
93      private static final Properties SETTINGS_PROPERTIES = new Properties() {
94          @Override
95          public synchronized Enumeration<Object> keys() {
96              Enumeration<Object> keysEnumeration = super.keys();
97              TreeSet<String> sortedKeySet = new TreeSet<>();
98              while (keysEnumeration.hasMoreElements()) {
99                  sortedKeySet.add((String) keysEnumeration.nextElement());
100             }
101             final Iterator<String> sortedKeysIterator = sortedKeySet.iterator();
102             return new Enumeration<Object>() {
103 
104                 public boolean hasMoreElements() {
105                     return sortedKeysIterator.hasNext();
106                 }
107 
108                 public Object nextElement() {
109                     return sortedKeysIterator.next();
110                 }
111             };
112         }
113 
114     };
115     private static String configFilePath;
116     private static boolean isFirstStart;
117 
118     /**
119      * Set config file path (from command line parameter).
120      *
121      * @param path davmail properties file path
122      */
123     public static synchronized void setConfigFilePath(String path) {
124         configFilePath = path;
125     }
126 
127     /**
128      * Detect first launch (properties file does not exist).
129      *
130      * @return true if this is the first start with the current file path
131      */
132     public static synchronized boolean isFirstStart() {
133         return isFirstStart;
134     }
135 
136     /**
137      * Load properties from provided stream (used in webapp mode).
138      *
139      * @param inputStream properties stream
140      * @throws IOException on error
141      */
142     public static synchronized void load(InputStream inputStream) throws IOException {
143         SETTINGS_PROPERTIES.load(inputStream);
144         updateLoggingConfig();
145     }
146 
147     /**
148      * Load properties from current file path (command line or default).
149      */
150     public static synchronized void load() {
151         try {
152             if (configFilePath == null) {
153                 //noinspection AccessOfSystemProperties
154                 configFilePath = System.getProperty("user.home") + "/.davmail.properties";
155                 if (!new File(configFilePath).exists()) {
156                     // .davmail.properties does not exist, switch to new freedesktop compliant location
157                     configFilePath = getXDGConfigFilePath();
158                 }
159             }
160             File configFile = new File(configFilePath);
161             if (configFile.exists()) {
162                 try (FileInputStream fileInputStream = new FileInputStream(configFile)) {
163                     load(fileInputStream);
164                 }
165             } else {
166                 isFirstStart = true;
167 
168                 // first start : set default values, ports above 1024 for unix/linux
169                 setDefaultSettings();
170                 save();
171             }
172         } catch (IOException e) {
173             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_LOAD_SETTINGS"), e);
174         }
175         updateLoggingConfig();
176     }
177 
178     /**
179      * Build an XDG compliant config file path.
180      *
181      * @return config file path
182      */
183     private static String getXDGConfigFilePath() {
184         String defaultConfigPath = System.getProperty("user.home")+".davmail.properties";
185         String xdgConfigDirectory = System.getenv("XDG_CONFIG_HOME");
186         if (xdgConfigDirectory == null) {
187             xdgConfigDirectory = System.getProperty("user.home")+"/.config";
188         }
189         File xdgConfigDirectoryFile = new File(xdgConfigDirectory);
190         if (!xdgConfigDirectoryFile.exists()) {
191             if (!xdgConfigDirectoryFile.mkdirs()) {
192                 return defaultConfigPath;
193             }
194         }
195         File xdgConfigDavMailDirectoryFile = new File(xdgConfigDirectory+"/davmail");
196         if (!xdgConfigDavMailDirectoryFile.exists()) {
197             if (!xdgConfigDavMailDirectoryFile.mkdirs()) {
198                 return defaultConfigPath;
199             }
200         }
201         return xdgConfigDirectory+"/davmail/davmail.properties";
202     }
203 
204     /**
205      * Set all settings to default values.
206      * Ports above 1024 for unix/linux
207      */
208     public static void setDefaultSettings() {
209         SETTINGS_PROPERTIES.put("davmail.mode", O365_EWS);
210         SETTINGS_PROPERTIES.put("davmail.authentication", O365_INTERACTIVE);
211         SETTINGS_PROPERTIES.put("davmail.url", getO365Url());
212         SETTINGS_PROPERTIES.put("davmail.popPort", "1110");
213         SETTINGS_PROPERTIES.put("davmail.imapPort", "1143");
214         SETTINGS_PROPERTIES.put("davmail.smtpPort", "1025");
215         SETTINGS_PROPERTIES.put("davmail.caldavPort", "1080");
216         SETTINGS_PROPERTIES.put("davmail.ldapPort", "1389");
217         SETTINGS_PROPERTIES.put("davmail.clientSoTimeout", "");
218         SETTINGS_PROPERTIES.put("davmail.keepDelay", "30");
219         SETTINGS_PROPERTIES.put("davmail.sentKeepDelay", "0");
220         SETTINGS_PROPERTIES.put("davmail.caldavPastDelay", "0");
221         SETTINGS_PROPERTIES.put("davmail.caldavAutoSchedule", Boolean.TRUE.toString());
222         SETTINGS_PROPERTIES.put("davmail.imapIdleDelay", "");
223         SETTINGS_PROPERTIES.put("davmail.folderSizeLimit", "");
224         SETTINGS_PROPERTIES.put("davmail.enableKeepAlive", Boolean.FALSE.toString());
225         SETTINGS_PROPERTIES.put("davmail.allowRemote", Boolean.FALSE.toString());
226         SETTINGS_PROPERTIES.put("davmail.bindAddress", "");
227         SETTINGS_PROPERTIES.put("davmail.useSystemProxies", Boolean.FALSE.toString());
228         SETTINGS_PROPERTIES.put("davmail.enableProxy", Boolean.FALSE.toString());
229         SETTINGS_PROPERTIES.put("davmail.enableKerberos", "false");
230         SETTINGS_PROPERTIES.put("davmail.disableUpdateCheck", "false");
231         SETTINGS_PROPERTIES.put("davmail.proxyHost", "");
232         SETTINGS_PROPERTIES.put("davmail.proxyPort", "");
233         SETTINGS_PROPERTIES.put("davmail.proxyUser", "");
234         SETTINGS_PROPERTIES.put("davmail.proxyPassword", "");
235         SETTINGS_PROPERTIES.put("davmail.noProxyFor", "");
236         SETTINGS_PROPERTIES.put("davmail.server", Boolean.FALSE.toString());
237         SETTINGS_PROPERTIES.put("davmail.server.certificate.hash", "");
238         SETTINGS_PROPERTIES.put("davmail.caldavAlarmSound", "");
239         SETTINGS_PROPERTIES.put("davmail.carddavReadPhoto", Boolean.TRUE.toString());
240         SETTINGS_PROPERTIES.put("davmail.forceActiveSyncUpdate", Boolean.FALSE.toString());
241         if (isLinux()) {
242             SETTINGS_PROPERTIES.put("davmail.enableTray", Boolean.FALSE.toString());
243         } else {
244             SETTINGS_PROPERTIES.put("davmail.enableTray", Boolean.TRUE.toString());
245         }
246         SETTINGS_PROPERTIES.put("davmail.showStartupBanner", Boolean.TRUE.toString());
247         SETTINGS_PROPERTIES.put("davmail.disableGuiNotifications", Boolean.FALSE.toString());
248         SETTINGS_PROPERTIES.put("davmail.disableTrayActivitySwitch", Boolean.FALSE.toString());
249         SETTINGS_PROPERTIES.put("davmail.imapAutoExpunge", Boolean.TRUE.toString());
250         SETTINGS_PROPERTIES.put("davmail.imapAlwaysApproxMsgSize", Boolean.FALSE.toString());
251         SETTINGS_PROPERTIES.put("davmail.popMarkReadOnRetr", Boolean.FALSE.toString());
252         SETTINGS_PROPERTIES.put("davmail.smtpSaveInSent", Boolean.TRUE.toString());
253         SETTINGS_PROPERTIES.put("davmail.ssl.keystoreType", "");
254         SETTINGS_PROPERTIES.put("davmail.ssl.keystoreFile", "");
255         SETTINGS_PROPERTIES.put("davmail.ssl.keystorePass", "");
256         SETTINGS_PROPERTIES.put("davmail.ssl.keyPass", "");
257         if (isWindows()) {
258             // default to MSCAPI on Windows for native client certificate access
259             SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreType", "MSCAPI");
260         } else {
261             SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreType", "");
262         }
263         SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreFile", "");
264         SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystorePass", "");
265         SETTINGS_PROPERTIES.put("davmail.ssl.pkcs11Library", "");
266         SETTINGS_PROPERTIES.put("davmail.ssl.pkcs11Config", "");
267         SETTINGS_PROPERTIES.put("davmail.ssl.nosecurepop", Boolean.FALSE.toString());
268         SETTINGS_PROPERTIES.put("davmail.ssl.nosecureimap", Boolean.FALSE.toString());
269         SETTINGS_PROPERTIES.put("davmail.ssl.nosecuresmtp", Boolean.FALSE.toString());
270         SETTINGS_PROPERTIES.put("davmail.ssl.nosecurecaldav", Boolean.FALSE.toString());
271         SETTINGS_PROPERTIES.put("davmail.ssl.nosecureldap", Boolean.FALSE.toString());
272 
273         // logging
274         SETTINGS_PROPERTIES.put("log4j.rootLogger", Level.WARN.toString());
275         SETTINGS_PROPERTIES.put("log4j.logger.davmail", Level.DEBUG.toString());
276         SETTINGS_PROPERTIES.put("log4j.logger.httpclient.wire", Level.WARN.toString());
277         SETTINGS_PROPERTIES.put("log4j.logger.httpclient", Level.WARN.toString());
278         String logFilePath = "";
279         if (isFlatpak()) {
280             logFilePath = System.getenv("XDG_DATA_HOME")+"/davmail.log";
281         }
282         SETTINGS_PROPERTIES.put("davmail.logFilePath", logFilePath);
283     }
284 
285     /**
286      * Return DavMail log file path
287      *
288      * @return full log file path
289      */
290     public static String getLogFilePath() {
291         String logFilePath = Settings.getProperty("davmail.logFilePath");
292         // set default log file path
293         if ((logFilePath == null || logFilePath.isEmpty())) {
294             if (Settings.getBooleanProperty("davmail.server")) {
295                 logFilePath = "davmail.log";
296             } else if (System.getProperty("os.name").toLowerCase().startsWith("mac os x")) {
297                 // store davmail.log in OSX Logs directory
298                 logFilePath = System.getProperty("user.home") + "/Library/Logs/DavMail/davmail.log";
299             } else {
300                 // store davmail.log in user home folder
301                 logFilePath = System.getProperty("user.home") + "/davmail.log";
302             }
303         } else {
304             File logFile = new File(logFilePath);
305             if (logFile.isDirectory()) {
306                 logFilePath += "/davmail.log";
307             }
308         }
309         return logFilePath;
310     }
311 
312     /**
313      * Return DavMail log file directory
314      *
315      * @return full log file directory
316      */
317     public static String getLogFileDirectory() {
318         String logFilePath = getLogFilePath();
319         if (logFilePath == null || logFilePath.isEmpty()) {
320             return ".";
321         }
322         int lastSlashIndex = logFilePath.lastIndexOf('/');
323         if (lastSlashIndex == -1) {
324             lastSlashIndex = logFilePath.lastIndexOf('\\');
325         }
326         if (lastSlashIndex >= 0) {
327             return logFilePath.substring(0, lastSlashIndex);
328         } else {
329             return ".";
330         }
331     }
332 
333     /**
334      * Update Log4J config from settings.
335      */
336     public static void updateLoggingConfig() {
337         String logFilePath = getLogFilePath();
338 
339         try {
340             if (!isDocker()) {
341                 if (logFilePath != null && !logFilePath.isEmpty()) {
342                     File logFile = new File(logFilePath);
343                     // create parent directory if needed
344                     File logFileDir = logFile.getParentFile();
345                     if (logFileDir != null && !logFileDir.exists() && (!logFileDir.mkdirs())) {
346                         DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_CREATE_LOG_FILE_DIR"));
347                         throw new IOException();
348 
349                     }
350                 } else {
351                     logFilePath = "davmail.log";
352                 }
353 
354                 synchronized (Logger.getRootLogger()) {
355                     // Build file appender
356                     FileAppender fileAppender = (FileAppender) Logger.getRootLogger().getAppender("FileAppender");
357                     if (fileAppender == null) {
358                         String logFileSize = Settings.getProperty("davmail.logFileSize");
359                         if (logFileSize == null || logFileSize.isEmpty()) {
360                             logFileSize = "1MB";
361                         }
362                         // set log file size to 0 to use an external rotation mechanism, e.g. logrotate
363                         if ("0".equals(logFileSize)) {
364                             fileAppender = new FileAppender();
365                         } else {
366                             fileAppender = new RollingFileAppender();
367                             ((RollingFileAppender) fileAppender).setMaxBackupIndex(2);
368                             ((RollingFileAppender) fileAppender).setMaxFileSize(logFileSize);
369                         }
370                         fileAppender.setName("FileAppender");
371                         fileAppender.setEncoding("UTF-8");
372                         fileAppender.setLayout(new PatternLayout("%d{ISO8601} %-5p [%t] %c %x - %m%n"));
373                     }
374                     fileAppender.setFile(logFilePath, true, false, 8192);
375                     Logger.getRootLogger().addAppender(fileAppender);
376 
377                     if (Settings.getProperty("davmail.logConnectionFilePath", null) != null) {
378                         FileAppender connectionFileAppender = (FileAppender) Logger.getLogger("davmail.connection").getAppender("ConnectionAppender");
379                         if (connectionFileAppender == null) {
380                             connectionFileAppender = new RollingFileAppender();
381                             ((RollingFileAppender) connectionFileAppender).setMaxBackupIndex(0);
382                             ((RollingFileAppender) connectionFileAppender).setMaxFileSize("10MB");
383 
384                             connectionFileAppender.setName("ConnectionAppender");
385                             connectionFileAppender.setEncoding("UTF-8");
386                             connectionFileAppender.setLayout(new PatternLayout("%d{ISO8601} %-5p [%t] %c %x - %m%n"));
387                         }
388                         connectionFileAppender.setFile(Settings.getProperty("davmail.logConnectionFilePath", null), true, false, 8192);
389 
390                         Logger.getLogger("davmail.connection").addAppender(connectionFileAppender);
391                     }
392                 }
393             }
394 
395             // disable ConsoleAppender in gui mode
396             ConsoleAppender consoleAppender = (ConsoleAppender) Logger.getRootLogger().getAppender("ConsoleAppender");
397             if (consoleAppender != null) {
398                 consoleAppender.setThreshold(Level.ALL);
399             }
400 
401         } catch (IOException e) {
402             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_SET_LOG_FILE_PATH"));
403         }
404 
405         // update logging levels
406         Settings.setLoggingLevel("rootLogger", Settings.getLoggingLevel("rootLogger"));
407         Settings.setLoggingLevel("davmail", Settings.getLoggingLevel("davmail"));
408         // set logging levels for HttpClient 4
409         Settings.setLoggingLevel("org.apache.http.wire", Settings.getLoggingLevel("httpclient.wire"));
410         Settings.setLoggingLevel("org.apache.http.conn.ssl", Settings.getLoggingLevel("httpclient.wire"));
411         Settings.setLoggingLevel("org.apache.http", Settings.getLoggingLevel("httpclient"));
412     }
413 
414     /**
415      * Save settings in current file path (command line or default).
416      */
417     public static synchronized void save() {
418         // configFilePath is null in some test cases
419         if (configFilePath != null) {
420             // clone settings
421             Properties properties = new Properties();
422             properties.putAll(SETTINGS_PROPERTIES);
423             // file lines
424             ArrayList<String> lines = new ArrayList<>();
425 
426             // try to make .davmail.properties file readable by user only on create
427             createPrivateFile(configFilePath);
428 
429             readLines(lines, properties);
430 
431             try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(Paths.get(configFilePath)), StandardCharsets.ISO_8859_1))) {
432                 for (String value : lines) {
433                     writer.write(value);
434                     writer.newLine();
435                 }
436 
437                 // write remaining lines
438                 Enumeration<?> propertyEnumeration = properties.propertyNames();
439                 while (propertyEnumeration.hasMoreElements()) {
440                     String propertyName = (String) propertyEnumeration.nextElement();
441                     writer.write(propertyName + "=" + escapeValue(properties.getProperty(propertyName)));
442                     writer.newLine();
443                 }
444             } catch (IOException e) {
445                 DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_STORE_SETTINGS"), e);
446             }
447         }
448         updateLoggingConfig();
449     }
450 
451     private static void createPrivateFile(String filePath) {
452         Path path = Paths.get(filePath);
453         if (!Files.exists(path)) {
454             try {
455                 if (isUnix() || isOSX()) {
456                     FileAttribute<?> permissions = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"));
457                     Files.createFile(path, permissions);
458                 } else {
459                     Files.createFile(path);
460                 }
461                 LOGGER.info("Created file " + filePath);
462             } catch (IOException e) {
463                 LOGGER.error(e.getMessage());
464             }
465         }
466     }
467 
468     private static void readLines(ArrayList<String> lines, Properties properties) {
469         try {
470             File configFile = new File(configFilePath);
471             if (configFile.exists()) {
472                 try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(configFile.toPath()), StandardCharsets.ISO_8859_1))) {
473                     String line;
474                     while ((line = reader.readLine()) != null) {
475                         lines.add(convertLine(line, properties));
476                     }
477                 }
478             }
479         } catch (IOException e) {
480             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_LOAD_SETTINGS"), e);
481         }
482     }
483 
484     /**
485      * Convert input property line to new line with value from properties.
486      * Preserve comments
487      *
488      * @param line       input line
489      * @param properties new property values
490      * @return new line
491      */
492     private static String convertLine(String line, Properties properties) {
493         int hashIndex = line.indexOf('#');
494         int equalsIndex = line.indexOf('=');
495         // allow # in values, no a comment
496         // comments are pass through
497         if (equalsIndex >= 0 && (hashIndex < 0 || hashIndex >= equalsIndex)) {
498             String key = line.substring(0, equalsIndex);
499             String value = properties.getProperty(key);
500             if (value != null) {
501                 // build property with new value
502                 line = key + "=" + escapeValue(value);
503                 // remove property from source
504                 properties.remove(key);
505             }
506         }
507         return line;
508     }
509 
510     /**
511      * Escape backslash in value.
512      *
513      * @param value value
514      * @return escaped value
515      */
516     private static String escapeValue(String value) {
517         StringBuilder buffer = new StringBuilder();
518         for (char c : value.toCharArray()) {
519             if (c == '\\') {
520                 buffer.append('\\');
521             }
522             buffer.append(c);
523         }
524         return buffer.toString();
525     }
526 
527 
528     /**
529      * Get a property value as String.
530      *
531      * @param property property name
532      * @return property value
533      */
534     public static synchronized String getProperty(String property) {
535         String value = SETTINGS_PROPERTIES.getProperty(property);
536         // return null on empty value
537         if (value != null && value.isEmpty()) {
538             value = null;
539         }
540         return value;
541     }
542 
543     /**
544      * Get property value or default.
545      *
546      * @param property     property name
547      * @param defaultValue default property value
548      * @return property value
549      */
550     public static synchronized String getProperty(String property, String defaultValue) {
551         String value = getProperty(property);
552         if (value == null) {
553             value = defaultValue;
554         }
555         return value;
556     }
557 
558 
559     /**
560      * Get a property value as char[].
561      *
562      * @param property property name
563      * @return property value
564      */
565     public static synchronized char[] getCharArrayProperty(String property) {
566         String propertyValue = Settings.getProperty(property);
567         char[] value = null;
568         if (propertyValue != null) {
569             value = propertyValue.toCharArray();
570         }
571         return value;
572     }
573 
574     /**
575      * Set a property value.
576      *
577      * @param property property name
578      * @param value    property value
579      */
580     public static synchronized void setProperty(String property, String value) {
581         if (value != null) {
582             SETTINGS_PROPERTIES.setProperty(property, value);
583         } else {
584             SETTINGS_PROPERTIES.setProperty(property, "");
585         }
586     }
587 
588     /**
589      * Get a property value as int.
590      *
591      * @param property property name
592      * @return property value
593      */
594     public static synchronized int getIntProperty(String property) {
595         return getIntProperty(property, 0);
596     }
597 
598     /**
599      * Get a property value as int, return default value if null.
600      *
601      * @param property     property name
602      * @param defaultValue default property value
603      * @return property value
604      */
605     public static synchronized int getIntProperty(String property, int defaultValue) {
606         int value = defaultValue;
607         try {
608             String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
609             if (propertyValue != null && !propertyValue.isEmpty()) {
610                 value = Integer.parseInt(propertyValue);
611             }
612         } catch (NumberFormatException e) {
613             DavGatewayTray.error(new BundleMessage("LOG_INVALID_SETTING_VALUE", property), e);
614         }
615         return value;
616     }
617 
618     /**
619      * Get a property value as boolean.
620      *
621      * @param property property name
622      * @return property value
623      */
624     public static synchronized boolean getBooleanProperty(String property) {
625         String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
626         return Boolean.parseBoolean(propertyValue);
627     }
628 
629     /**
630      * Get a property value as boolean.
631      *
632      * @param property     property name
633      * @param defaultValue default property value
634      * @return property value
635      */
636     public static synchronized boolean getBooleanProperty(String property, boolean defaultValue) {
637         boolean value = defaultValue;
638         String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
639         if (propertyValue != null && !propertyValue.isEmpty()) {
640             value = Boolean.parseBoolean(propertyValue);
641         }
642         return value;
643     }
644 
645     public static synchronized String loadRefreshToken(String username) {
646         String tokenFilePath = Settings.getProperty("davmail.oauth.tokenFilePath");
647         if (isEmpty(tokenFilePath)) {
648             return Settings.getProperty("davmail.oauth." + username.toLowerCase() + ".refreshToken");
649         } else {
650             return loadTokenFromFile(tokenFilePath, username.toLowerCase());
651         }
652     }
653 
654 
655     public static synchronized void storeRefreshToken(String username, String refreshToken) {
656         String tokenFilePath = Settings.getProperty("davmail.oauth.tokenFilePath");
657         if (isEmpty(tokenFilePath)) {
658             Settings.setProperty("davmail.oauth." + username.toLowerCase() + ".refreshToken", refreshToken);
659             Settings.save();
660         } else {
661             saveTokenToFile(tokenFilePath, username.toLowerCase(), refreshToken);
662         }
663     }
664 
665     /**
666      * Persist token in davmail.oauth.tokenFilePath.
667      *
668      * @param tokenFilePath token file path
669      * @param username      username
670      * @param refreshToken  Oauth refresh token
671      */
672     private static void saveTokenToFile(String tokenFilePath, String username, String refreshToken) {
673         try {
674             checkCreateTokenFilePath(tokenFilePath);
675             Properties properties = new Properties();
676             try (FileInputStream fis = new FileInputStream(tokenFilePath)) {
677                 properties.load(fis);
678             }
679             properties.setProperty(username, refreshToken);
680             try (FileOutputStream fos = new FileOutputStream(tokenFilePath)) {
681                 properties.store(fos, "Oauth tokens");
682             }
683         } catch (IOException e) {
684             Logger.getLogger(Settings.class).warn(e + " " + e.getMessage());
685         }
686     }
687 
688     /**
689      * Load token from davmail.oauth.tokenFilePath.
690      *
691      * @param tokenFilePath token file path
692      * @param username      username
693      * @return encrypted token value
694      */
695     private static String loadTokenFromFile(String tokenFilePath, String username) {
696         try {
697             checkCreateTokenFilePath(tokenFilePath);
698             Properties properties = new Properties();
699             try (FileInputStream fis = new FileInputStream(tokenFilePath)) {
700                 properties.load(fis);
701             }
702             return properties.getProperty(username);
703         } catch (IOException e) {
704             Logger.getLogger(Settings.class).warn(e + " " + e.getMessage());
705         }
706         return null;
707     }
708 
709     private static void checkCreateTokenFilePath(String tokenFilePath) throws IOException {
710         File file = new File(tokenFilePath);
711         File parentFile = file.getParentFile();
712         if (parentFile != null && (parentFile.mkdirs())) {
713             LOGGER.info("Created token file directory " + parentFile.getAbsolutePath());
714         }
715         createPrivateFile(tokenFilePath);
716     }
717 
718     /**
719      * Build logging properties prefix.
720      *
721      * @param category logging category
722      * @return prefix
723      */
724     private static String getLoggingPrefix(String category) {
725         String prefix;
726         if ("rootLogger".equals(category)) {
727             prefix = "log4j.";
728         } else {
729             prefix = "log4j.logger.";
730         }
731         return prefix;
732     }
733 
734     /**
735      * Return Log4J logging level for the category.
736      *
737      * @param category logging category
738      * @return logging level
739      */
740     public static synchronized Level getLoggingLevel(String category) {
741         String prefix = getLoggingPrefix(category);
742         String currentValue = SETTINGS_PROPERTIES.getProperty(prefix + category);
743 
744         if (currentValue != null && !currentValue.isEmpty()) {
745             return Level.toLevel(currentValue);
746         } else if ("rootLogger".equals(category)) {
747             return Logger.getRootLogger().getLevel();
748         } else {
749             return Logger.getLogger(category).getLevel();
750         }
751     }
752 
753     /**
754      * Get all properties that are in the specified scope, that is, that start with '&lt;scope&gt;.'.
755      *
756      * @param scope start of property name
757      * @return properties
758      */
759     public static synchronized Properties getSubProperties(String scope) {
760         final String keyStart;
761         if (scope == null || scope.isEmpty()) {
762             keyStart = "";
763         } else if (scope.endsWith(".")) {
764             keyStart = scope;
765         } else {
766             keyStart = scope + '.';
767         }
768         Properties result = new Properties();
769         for (Map.Entry<Object, Object> entry : SETTINGS_PROPERTIES.entrySet()) {
770             String key = (String) entry.getKey();
771             if (key.startsWith(keyStart)) {
772                 String value = (String) entry.getValue();
773                 result.setProperty(key.substring(keyStart.length()), value);
774             }
775         }
776         return result;
777     }
778 
779     /**
780      * Set Log4J logging level for the category
781      *
782      * @param category logging category
783      * @param level    logging level
784      */
785     public static synchronized void setLoggingLevel(String category, Level level) {
786         if (level != null) {
787             String prefix = getLoggingPrefix(category);
788             SETTINGS_PROPERTIES.setProperty(prefix + category, level.toString());
789             if ("rootLogger".equals(category)) {
790                 Logger.getRootLogger().setLevel(level);
791             } else {
792                 Logger.getLogger(category).setLevel(level);
793             }
794         }
795     }
796 
797     /**
798      * Change and save a single property.
799      *
800      * @param property property name
801      * @param value    property value
802      */
803     public static synchronized void saveProperty(String property, String value) {
804         Settings.load();
805         Settings.setProperty(property, value);
806         Settings.save();
807     }
808 
809     /**
810      * Test if running on Windows
811      *
812      * @return true on Windows
813      */
814     public static boolean isWindows() {
815         return System.getProperty("os.name").toLowerCase().startsWith("windows");
816     }
817 
818     /**
819      * Test if running on OSX
820      *
821      * @return true on Mac OS X
822      */
823     public static boolean isOSX() {
824         return System.getProperty("os.name").toLowerCase().startsWith("mac os x");
825     }
826 
827     /**
828      * Test if running on Linux
829      *
830      * @return true on Linux
831      */
832     public static boolean isLinux() {
833         return System.getProperty("os.name").toLowerCase().startsWith("linux");
834     }
835 
836     public static boolean isUnix() {
837         return isLinux() ||
838                 System.getProperty("os.name").toLowerCase().startsWith("freebsd");
839     }
840 
841     public static String getUserAgent() {
842         return getProperty("davmail.userAgent", Settings.EDGE_USER_AGENT);
843     }
844 
845     public static String getOutlookUrl() {
846         String tld = getProperty("davmail.tld");
847         String outlookUrl = getProperty("davmail.outlookUrl");
848         if (outlookUrl != null) {
849             return outlookUrl;
850         } else if (tld == null) {
851             return OUTLOOK_URL;
852         } else {
853             return "https://outlook.office365." + tld;
854         }
855     }
856 
857     public static String getO365Url() {
858         String tld = getProperty("davmail.tld");
859         String outlookUrl = getProperty("davmail.outlookUrl");
860         if (outlookUrl != null) {
861             return outlookUrl + "/EWS/Exchange.asmx";
862         } else if (tld == null) {
863             return O365_URL;
864         } else {
865             return "https://outlook.office365." + tld + "/EWS/Exchange.asmx";
866         }
867     }
868 
869     /**
870      * Handle custom graph endpoints.
871      * See <a href="https://learn.microsoft.com/en-us/graph/deployments">...</a>
872      * @return graph endpoint url
873      */
874     public static String getGraphUrl() {
875         String tld = getProperty("davmail.tld");
876         String graphUrl = getProperty("davmail.graphUrl");
877         if (graphUrl != null) {
878             return graphUrl;
879         } else if (tld == null) {
880             return GRAPH_URL;
881         } else {
882             return "https://graph.microsoft." + tld;
883         }
884     }
885 
886     public static String getO365LoginUrl() {
887         String tld = getProperty("davmail.tld");
888         String loginUrl = getProperty("davmail.loginUrl");
889         if (loginUrl != null) {
890             return loginUrl;
891         } else if (tld == null) {
892             return O365_LOGIN_URL;
893         } else {
894             return "https://login.microsoftonline." + tld;
895         }
896     }
897 
898     public static String getOauthScope() {
899         String defaultOauthScope = "openid profile offline_access Mail.ReadWrite Calendars.ReadWrite Calendars.ReadWrite.Shared MailboxSettings.Read Mail.ReadWrite.Shared Contacts.ReadWrite Tasks.ReadWrite Mail.Send People.Read";
900         if (Settings.getProperty("davmail.oauth.redirectUri","").startsWith("urn:")) {
901             defaultOauthScope = "openid profile offline_access";
902         }
903         return Settings.getProperty("davmail.oauth.scope", defaultOauthScope);
904     }
905 
906     public static boolean isGraphEnabled() {
907         return Settings.O365_GRAPH.equals(Settings.getProperty("davmail.mode")) || Settings.getBooleanProperty("davmail.enableGraph");
908     }
909 
910     public static boolean isSWTAvailable() {
911         boolean isSWTAvailable = false;
912         ClassLoader classloader = Settings.class.getClassLoader();
913         try {
914             // trigger ClassNotFoundException
915             classloader.loadClass("org.eclipse.swt.SWT");
916             isSWTAvailable = true;
917         } catch (Throwable e) {
918             // do not log on OSX, using standard system tray by default
919             if (!isOSX()) {
920                 LOGGER.info(new BundleMessage("LOG_SWT_NOT_AVAILABLE"));
921             }
922         }
923         return isSWTAvailable;
924     }
925 
926     public static boolean isJFXAvailable() {
927         boolean isJFXAvailable = false;
928         try {
929             Class.forName("javafx.application.Platform");
930             isJFXAvailable = true;
931         } catch (ClassNotFoundException | NullPointerException e) {
932             LOGGER.info("JavaFX (OpenJFX) not available");
933         }
934         return isJFXAvailable;
935     }
936 
937     public static boolean isDocker() {
938         boolean isDocker = new File("/.dockerenv").exists();
939         boolean isPodman = new File("/run/.containerenv").exists();
940         if (isDocker) {
941             LOGGER.info("Running in docker or podman");
942         }
943         return isDocker || isPodman;
944     }
945 
946     public static boolean isFlatpak() {
947         boolean isFlatpak = "flatpak".equals(System.getenv("container"));
948         if (isFlatpak) {
949             LOGGER.info("Running in Flatpak");
950         }
951         return isFlatpak;
952     }
953 
954     /**
955      * Set davmail properties path in Docker and Flatpak
956      * @return davmail.properties path
957      */
958     public static synchronized String getConfigFilePath() {
959         if (configFilePath != null) {
960             return configFilePath;
961         } else if (isFlatpak()) {
962             return System.getenv("XDG_CONFIG_HOME")+"/davmail.properties";
963         } else {
964             // Docker
965             return System.getenv("DAVMAIL_PROPERTIES");
966         }
967     }
968 }