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      public static final String O365 = "O365";
70      public static final String O365_MODERN = "O365Modern";
71      public static final String O365_INTERACTIVE = "O365Interactive";
72      public static final String O365_MANUAL = "O365Manual";
73      public static final String WEBDAV = "WebDav";
74      public static final String EWS = "EWS";
75      public static final String AUTO = "Auto";
76  
77      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";
78  
79      private Settings() {
80      }
81  
82      private static final Properties SETTINGS_PROPERTIES = new Properties() {
83          @Override
84          public synchronized Enumeration<Object> keys() {
85              Enumeration<Object> keysEnumeration = super.keys();
86              TreeSet<String> sortedKeySet = new TreeSet<>();
87              while (keysEnumeration.hasMoreElements()) {
88                  sortedKeySet.add((String) keysEnumeration.nextElement());
89              }
90              final Iterator<String> sortedKeysIterator = sortedKeySet.iterator();
91              return new Enumeration<Object>() {
92  
93                  public boolean hasMoreElements() {
94                      return sortedKeysIterator.hasNext();
95                  }
96  
97                  public Object nextElement() {
98                      return sortedKeysIterator.next();
99                  }
100             };
101         }
102 
103     };
104     private static String configFilePath;
105     private static boolean isFirstStart;
106 
107     /**
108      * Set config file path (from command line parameter).
109      *
110      * @param path davmail properties file path
111      */
112     public static synchronized void setConfigFilePath(String path) {
113         configFilePath = path;
114     }
115 
116     /**
117      * Detect first launch (properties file does not exist).
118      *
119      * @return true if this is the first start with the current file path
120      */
121     public static synchronized boolean isFirstStart() {
122         return isFirstStart;
123     }
124 
125     /**
126      * Load properties from provided stream (used in webapp mode).
127      *
128      * @param inputStream properties stream
129      * @throws IOException on error
130      */
131     public static synchronized void load(InputStream inputStream) throws IOException {
132         SETTINGS_PROPERTIES.load(inputStream);
133         updateLoggingConfig();
134     }
135 
136     /**
137      * Load properties from current file path (command line or default).
138      */
139     public static synchronized void load() {
140         try {
141             if (configFilePath == null) {
142                 //noinspection AccessOfSystemProperties
143                 configFilePath = System.getProperty("user.home") + "/.davmail.properties";
144             }
145             File configFile = new File(configFilePath);
146             if (configFile.exists()) {
147                 try (FileInputStream fileInputStream = new FileInputStream(configFile)) {
148                     load(fileInputStream);
149                 }
150             } else {
151                 isFirstStart = true;
152 
153                 // first start : set default values, ports above 1024 for unix/linux
154                 setDefaultSettings();
155                 save();
156             }
157         } catch (IOException e) {
158             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_LOAD_SETTINGS"), e);
159         }
160         updateLoggingConfig();
161     }
162 
163     /**
164      * Set all settings to default values.
165      * Ports above 1024 for unix/linux
166      */
167     public static void setDefaultSettings() {
168         SETTINGS_PROPERTIES.put("davmail.mode", O365_INTERACTIVE);
169         SETTINGS_PROPERTIES.put("davmail.url", getO365Url());
170         SETTINGS_PROPERTIES.put("davmail.popPort", "1110");
171         SETTINGS_PROPERTIES.put("davmail.imapPort", "1143");
172         SETTINGS_PROPERTIES.put("davmail.smtpPort", "1025");
173         SETTINGS_PROPERTIES.put("davmail.caldavPort", "1080");
174         SETTINGS_PROPERTIES.put("davmail.ldapPort", "1389");
175         SETTINGS_PROPERTIES.put("davmail.clientSoTimeout", "");
176         SETTINGS_PROPERTIES.put("davmail.keepDelay", "30");
177         SETTINGS_PROPERTIES.put("davmail.sentKeepDelay", "0");
178         SETTINGS_PROPERTIES.put("davmail.caldavPastDelay", "0");
179         SETTINGS_PROPERTIES.put("davmail.caldavAutoSchedule", Boolean.TRUE.toString());
180         SETTINGS_PROPERTIES.put("davmail.imapIdleDelay", "");
181         SETTINGS_PROPERTIES.put("davmail.folderSizeLimit", "");
182         SETTINGS_PROPERTIES.put("davmail.enableKeepAlive", Boolean.FALSE.toString());
183         SETTINGS_PROPERTIES.put("davmail.allowRemote", Boolean.FALSE.toString());
184         SETTINGS_PROPERTIES.put("davmail.bindAddress", "");
185         SETTINGS_PROPERTIES.put("davmail.useSystemProxies", Boolean.FALSE.toString());
186         SETTINGS_PROPERTIES.put("davmail.enableProxy", Boolean.FALSE.toString());
187         SETTINGS_PROPERTIES.put("davmail.enableKerberos", "false");
188         SETTINGS_PROPERTIES.put("davmail.disableUpdateCheck", "false");
189         SETTINGS_PROPERTIES.put("davmail.proxyHost", "");
190         SETTINGS_PROPERTIES.put("davmail.proxyPort", "");
191         SETTINGS_PROPERTIES.put("davmail.proxyUser", "");
192         SETTINGS_PROPERTIES.put("davmail.proxyPassword", "");
193         SETTINGS_PROPERTIES.put("davmail.noProxyFor", "");
194         SETTINGS_PROPERTIES.put("davmail.server", Boolean.FALSE.toString());
195         SETTINGS_PROPERTIES.put("davmail.server.certificate.hash", "");
196         SETTINGS_PROPERTIES.put("davmail.caldavAlarmSound", "");
197         SETTINGS_PROPERTIES.put("davmail.carddavReadPhoto", Boolean.TRUE.toString());
198         SETTINGS_PROPERTIES.put("davmail.forceActiveSyncUpdate", Boolean.FALSE.toString());
199         SETTINGS_PROPERTIES.put("davmail.showStartupBanner", Boolean.TRUE.toString());
200         SETTINGS_PROPERTIES.put("davmail.disableGuiNotifications", Boolean.FALSE.toString());
201         SETTINGS_PROPERTIES.put("davmail.disableTrayActivitySwitch", Boolean.FALSE.toString());
202         SETTINGS_PROPERTIES.put("davmail.imapAutoExpunge", Boolean.TRUE.toString());
203         SETTINGS_PROPERTIES.put("davmail.imapAlwaysApproxMsgSize", Boolean.FALSE.toString());
204         SETTINGS_PROPERTIES.put("davmail.popMarkReadOnRetr", Boolean.FALSE.toString());
205         SETTINGS_PROPERTIES.put("davmail.smtpSaveInSent", Boolean.TRUE.toString());
206         SETTINGS_PROPERTIES.put("davmail.ssl.keystoreType", "");
207         SETTINGS_PROPERTIES.put("davmail.ssl.keystoreFile", "");
208         SETTINGS_PROPERTIES.put("davmail.ssl.keystorePass", "");
209         SETTINGS_PROPERTIES.put("davmail.ssl.keyPass", "");
210         if (isWindows()) {
211             // default to MSCAPI on windows for native client certificate access
212             SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreType", "MSCAPI");
213         } else {
214             SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreType", "");
215         }
216         SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystoreFile", "");
217         SETTINGS_PROPERTIES.put("davmail.ssl.clientKeystorePass", "");
218         SETTINGS_PROPERTIES.put("davmail.ssl.pkcs11Library", "");
219         SETTINGS_PROPERTIES.put("davmail.ssl.pkcs11Config", "");
220         SETTINGS_PROPERTIES.put("davmail.ssl.nosecurepop", Boolean.FALSE.toString());
221         SETTINGS_PROPERTIES.put("davmail.ssl.nosecureimap", Boolean.FALSE.toString());
222         SETTINGS_PROPERTIES.put("davmail.ssl.nosecuresmtp", Boolean.FALSE.toString());
223         SETTINGS_PROPERTIES.put("davmail.ssl.nosecurecaldav", Boolean.FALSE.toString());
224         SETTINGS_PROPERTIES.put("davmail.ssl.nosecureldap", Boolean.FALSE.toString());
225 
226         // logging
227         SETTINGS_PROPERTIES.put("log4j.rootLogger", Level.WARN.toString());
228         SETTINGS_PROPERTIES.put("log4j.logger.davmail", Level.DEBUG.toString());
229         SETTINGS_PROPERTIES.put("log4j.logger.httpclient.wire", Level.WARN.toString());
230         SETTINGS_PROPERTIES.put("log4j.logger.httpclient", Level.WARN.toString());
231         String logFilePath = "";
232         if (isFlatpak()) {
233             logFilePath = System.getenv("XDG_DATA_HOME")+"/davmail.log";
234         }
235         SETTINGS_PROPERTIES.put("davmail.logFilePath", logFilePath);
236     }
237 
238     /**
239      * Return DavMail log file path
240      *
241      * @return full log file path
242      */
243     public static String getLogFilePath() {
244         String logFilePath = Settings.getProperty("davmail.logFilePath");
245         // set default log file path
246         if ((logFilePath == null || logFilePath.isEmpty())) {
247             if (Settings.getBooleanProperty("davmail.server")) {
248                 logFilePath = "davmail.log";
249             } else if (System.getProperty("os.name").toLowerCase().startsWith("mac os x")) {
250                 // store davmail.log in OSX Logs directory
251                 logFilePath = System.getProperty("user.home") + "/Library/Logs/DavMail/davmail.log";
252             } else {
253                 // store davmail.log in user home folder
254                 logFilePath = System.getProperty("user.home") + "/davmail.log";
255             }
256         } else {
257             File logFile = new File(logFilePath);
258             if (logFile.isDirectory()) {
259                 logFilePath += "/davmail.log";
260             }
261         }
262         return logFilePath;
263     }
264 
265     /**
266      * Return DavMail log file directory
267      *
268      * @return full log file directory
269      */
270     public static String getLogFileDirectory() {
271         String logFilePath = getLogFilePath();
272         if (logFilePath == null || logFilePath.isEmpty()) {
273             return ".";
274         }
275         int lastSlashIndex = logFilePath.lastIndexOf('/');
276         if (lastSlashIndex == -1) {
277             lastSlashIndex = logFilePath.lastIndexOf('\\');
278         }
279         if (lastSlashIndex >= 0) {
280             return logFilePath.substring(0, lastSlashIndex);
281         } else {
282             return ".";
283         }
284     }
285 
286     /**
287      * Update Log4J config from settings.
288      */
289     public static void updateLoggingConfig() {
290         String logFilePath = getLogFilePath();
291 
292         try {
293             if (!isDocker()) {
294                 if (logFilePath != null && !logFilePath.isEmpty()) {
295                     File logFile = new File(logFilePath);
296                     // create parent directory if needed
297                     File logFileDir = logFile.getParentFile();
298                     if (logFileDir != null && !logFileDir.exists() && (!logFileDir.mkdirs())) {
299                         DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_CREATE_LOG_FILE_DIR"));
300                         throw new IOException();
301 
302                     }
303                 } else {
304                     logFilePath = "davmail.log";
305                 }
306 
307                 synchronized (Logger.getRootLogger()) {
308                     // Build file appender
309                     FileAppender fileAppender = (FileAppender) Logger.getRootLogger().getAppender("FileAppender");
310                     if (fileAppender == null) {
311                         String logFileSize = Settings.getProperty("davmail.logFileSize");
312                         if (logFileSize == null || logFileSize.isEmpty()) {
313                             logFileSize = "1MB";
314                         }
315                         // set log file size to 0 to use an external rotation mechanism, e.g. logrotate
316                         if ("0".equals(logFileSize)) {
317                             fileAppender = new FileAppender();
318                         } else {
319                             fileAppender = new RollingFileAppender();
320                             ((RollingFileAppender) fileAppender).setMaxBackupIndex(2);
321                             ((RollingFileAppender) fileAppender).setMaxFileSize(logFileSize);
322                         }
323                         fileAppender.setName("FileAppender");
324                         fileAppender.setEncoding("UTF-8");
325                         fileAppender.setLayout(new PatternLayout("%d{ISO8601} %-5p [%t] %c %x - %m%n"));
326                     }
327                     fileAppender.setFile(logFilePath, true, false, 8192);
328                     Logger.getRootLogger().addAppender(fileAppender);
329                 }
330             }
331 
332             // disable ConsoleAppender in gui mode
333             ConsoleAppender consoleAppender = (ConsoleAppender) Logger.getRootLogger().getAppender("ConsoleAppender");
334             if (consoleAppender != null) {
335                 if (Settings.getBooleanProperty("davmail.server")) {
336                     consoleAppender.setThreshold(Level.ALL);
337                 } else {
338                     consoleAppender.setThreshold(Level.INFO);
339                 }
340             }
341 
342         } catch (IOException e) {
343             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_SET_LOG_FILE_PATH"));
344         }
345 
346         // update logging levels
347         Settings.setLoggingLevel("rootLogger", Settings.getLoggingLevel("rootLogger"));
348         Settings.setLoggingLevel("davmail", Settings.getLoggingLevel("davmail"));
349         // set logging levels for HttpClient 4
350         Settings.setLoggingLevel("org.apache.http.wire", Settings.getLoggingLevel("httpclient.wire"));
351         Settings.setLoggingLevel("org.apache.http.conn.ssl", Settings.getLoggingLevel("httpclient.wire"));
352         Settings.setLoggingLevel("org.apache.http", Settings.getLoggingLevel("httpclient"));
353     }
354 
355     /**
356      * Save settings in current file path (command line or default).
357      */
358     public static synchronized void save() {
359         // configFilePath is null in some test cases
360         if (configFilePath != null) {
361             // clone settings
362             Properties properties = new Properties();
363             properties.putAll(SETTINGS_PROPERTIES);
364             // file lines
365             ArrayList<String> lines = new ArrayList<>();
366 
367             // try to make .davmail.properties file readable by user only on create
368             Path path = Paths.get(configFilePath);
369             if (!Files.exists(path) && isUnix()) {
370                 FileAttribute<?> permissions = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"));
371                 try {
372                     Files.createFile(path, permissions);
373                 } catch (IOException e) {
374                     LOGGER.error(e.getMessage());
375                 }
376             }
377 
378             readLines(lines, properties);
379 
380             try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(Paths.get(configFilePath)), StandardCharsets.ISO_8859_1))) {
381                 for (String value : lines) {
382                     writer.write(value);
383                     writer.newLine();
384                 }
385 
386                 // write remaining lines
387                 Enumeration<?> propertyEnumeration = properties.propertyNames();
388                 while (propertyEnumeration.hasMoreElements()) {
389                     String propertyName = (String) propertyEnumeration.nextElement();
390                     writer.write(propertyName + "=" + escapeValue(properties.getProperty(propertyName)));
391                     writer.newLine();
392                 }
393             } catch (IOException e) {
394                 DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_STORE_SETTINGS"), e);
395             }
396         }
397         updateLoggingConfig();
398     }
399 
400     private static void readLines(ArrayList<String> lines, Properties properties) {
401         try {
402             File configFile = new File(configFilePath);
403             if (configFile.exists()) {
404                 try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(configFile.toPath()), StandardCharsets.ISO_8859_1))) {
405                     String line;
406                     while ((line = reader.readLine()) != null) {
407                         lines.add(convertLine(line, properties));
408                     }
409                 }
410             }
411         } catch (IOException e) {
412             DavGatewayTray.error(new BundleMessage("LOG_UNABLE_TO_LOAD_SETTINGS"), e);
413         }
414     }
415 
416     /**
417      * Convert input property line to new line with value from properties.
418      * Preserve comments
419      *
420      * @param line       input line
421      * @param properties new property values
422      * @return new line
423      */
424     private static String convertLine(String line, Properties properties) {
425         int hashIndex = line.indexOf('#');
426         int equalsIndex = line.indexOf('=');
427         // allow # in values, no a comment
428         // comments are pass through
429         if (equalsIndex >= 0 && (hashIndex < 0 || hashIndex >= equalsIndex)) {
430             String key = line.substring(0, equalsIndex);
431             String value = properties.getProperty(key);
432             if (value != null) {
433                 // build property with new value
434                 line = key + "=" + escapeValue(value);
435                 // remove property from source
436                 properties.remove(key);
437             }
438         }
439         return line;
440     }
441 
442     /**
443      * Escape backslash in value.
444      *
445      * @param value value
446      * @return escaped value
447      */
448     private static String escapeValue(String value) {
449         StringBuilder buffer = new StringBuilder();
450         for (char c : value.toCharArray()) {
451             if (c == '\\') {
452                 buffer.append('\\');
453             }
454             buffer.append(c);
455         }
456         return buffer.toString();
457     }
458 
459 
460     /**
461      * Get a property value as String.
462      *
463      * @param property property name
464      * @return property value
465      */
466     public static synchronized String getProperty(String property) {
467         String value = SETTINGS_PROPERTIES.getProperty(property);
468         // return null on empty value
469         if (value != null && value.isEmpty()) {
470             value = null;
471         }
472         return value;
473     }
474 
475     /**
476      * Get property value or default.
477      *
478      * @param property     property name
479      * @param defaultValue default property value
480      * @return property value
481      */
482     public static synchronized String getProperty(String property, String defaultValue) {
483         String value = getProperty(property);
484         if (value == null) {
485             value = defaultValue;
486         }
487         return value;
488     }
489 
490 
491     /**
492      * Get a property value as char[].
493      *
494      * @param property property name
495      * @return property value
496      */
497     public static synchronized char[] getCharArrayProperty(String property) {
498         String propertyValue = Settings.getProperty(property);
499         char[] value = null;
500         if (propertyValue != null) {
501             value = propertyValue.toCharArray();
502         }
503         return value;
504     }
505 
506     /**
507      * Set a property value.
508      *
509      * @param property property name
510      * @param value    property value
511      */
512     public static synchronized void setProperty(String property, String value) {
513         if (value != null) {
514             SETTINGS_PROPERTIES.setProperty(property, value);
515         } else {
516             SETTINGS_PROPERTIES.setProperty(property, "");
517         }
518     }
519 
520     /**
521      * Get a property value as int.
522      *
523      * @param property property name
524      * @return property value
525      */
526     public static synchronized int getIntProperty(String property) {
527         return getIntProperty(property, 0);
528     }
529 
530     /**
531      * Get a property value as int, return default value if null.
532      *
533      * @param property     property name
534      * @param defaultValue default property value
535      * @return property value
536      */
537     public static synchronized int getIntProperty(String property, int defaultValue) {
538         int value = defaultValue;
539         try {
540             String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
541             if (propertyValue != null && !propertyValue.isEmpty()) {
542                 value = Integer.parseInt(propertyValue);
543             }
544         } catch (NumberFormatException e) {
545             DavGatewayTray.error(new BundleMessage("LOG_INVALID_SETTING_VALUE", property), e);
546         }
547         return value;
548     }
549 
550     /**
551      * Get a property value as boolean.
552      *
553      * @param property property name
554      * @return property value
555      */
556     public static synchronized boolean getBooleanProperty(String property) {
557         String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
558         return Boolean.parseBoolean(propertyValue);
559     }
560 
561     /**
562      * Get a property value as boolean.
563      *
564      * @param property     property name
565      * @param defaultValue default property value
566      * @return property value
567      */
568     public static synchronized boolean getBooleanProperty(String property, boolean defaultValue) {
569         boolean value = defaultValue;
570         String propertyValue = SETTINGS_PROPERTIES.getProperty(property);
571         if (propertyValue != null && !propertyValue.isEmpty()) {
572             value = Boolean.parseBoolean(propertyValue);
573         }
574         return value;
575     }
576 
577     public static synchronized String loadRefreshToken(String username) {
578         String tokenFilePath = Settings.getProperty("davmail.oauth.tokenFilePath");
579         if (isEmpty(tokenFilePath)) {
580             return Settings.getProperty("davmail.oauth." + username.toLowerCase() + ".refreshToken");
581         } else {
582             return loadtokenFromFile(tokenFilePath, username.toLowerCase());
583         }
584     }
585 
586 
587     public static synchronized void storeRefreshToken(String username, String refreshToken) {
588         String tokenFilePath = Settings.getProperty("davmail.oauth.tokenFilePath");
589         if (isEmpty(tokenFilePath)) {
590             Settings.setProperty("davmail.oauth." + username.toLowerCase() + ".refreshToken", refreshToken);
591             Settings.save();
592         } else {
593             savetokentoFile(tokenFilePath, username.toLowerCase(), refreshToken);
594         }
595     }
596 
597     /**
598      * Persist token in davmail.oauth.tokenFilePath.
599      *
600      * @param tokenFilePath token file path
601      * @param username      username
602      * @param refreshToken  Oauth refresh token
603      */
604     private static void savetokentoFile(String tokenFilePath, String username, String refreshToken) {
605         try {
606             checkCreateTokenFilePath(tokenFilePath);
607             Properties properties = new Properties();
608             try (FileInputStream fis = new FileInputStream(tokenFilePath)) {
609                 properties.load(fis);
610             }
611             properties.setProperty(username, refreshToken);
612             try (FileOutputStream fos = new FileOutputStream(tokenFilePath)) {
613                 properties.store(fos, "Oauth tokens");
614             }
615         } catch (IOException e) {
616             Logger.getLogger(Settings.class).warn(e + " " + e.getMessage());
617         }
618     }
619 
620     /**
621      * Load token from davmail.oauth.tokenFilePath.
622      *
623      * @param tokenFilePath token file path
624      * @param username      username
625      * @return encrypted token value
626      */
627     private static String loadtokenFromFile(String tokenFilePath, String username) {
628         try {
629             checkCreateTokenFilePath(tokenFilePath);
630             Properties properties = new Properties();
631             try (FileInputStream fis = new FileInputStream(tokenFilePath)) {
632                 properties.load(fis);
633             }
634             return properties.getProperty(username);
635         } catch (IOException e) {
636             Logger.getLogger(Settings.class).warn(e + " " + e.getMessage());
637         }
638         return null;
639     }
640 
641     private static void checkCreateTokenFilePath(String tokenFilePath) throws IOException {
642         File file = new File(tokenFilePath);
643         File parentFile = file.getParentFile();
644         if (parentFile != null && (parentFile.mkdirs())) {
645             LOGGER.info("Created token file directory " + parentFile.getAbsolutePath());
646 
647         }
648         if (file.createNewFile()) {
649             LOGGER.info("Created token file " + tokenFilePath);
650         }
651     }
652 
653     /**
654      * Build logging properties prefix.
655      *
656      * @param category logging category
657      * @return prefix
658      */
659     private static String getLoggingPrefix(String category) {
660         String prefix;
661         if ("rootLogger".equals(category)) {
662             prefix = "log4j.";
663         } else {
664             prefix = "log4j.logger.";
665         }
666         return prefix;
667     }
668 
669     /**
670      * Return Log4J logging level for the category.
671      *
672      * @param category logging category
673      * @return logging level
674      */
675     public static synchronized Level getLoggingLevel(String category) {
676         String prefix = getLoggingPrefix(category);
677         String currentValue = SETTINGS_PROPERTIES.getProperty(prefix + category);
678 
679         if (currentValue != null && !currentValue.isEmpty()) {
680             return Level.toLevel(currentValue);
681         } else if ("rootLogger".equals(category)) {
682             return Logger.getRootLogger().getLevel();
683         } else {
684             return Logger.getLogger(category).getLevel();
685         }
686     }
687 
688     /**
689      * Get all properties that are in the specified scope, that is, that start with '&lt;scope&gt;.'.
690      *
691      * @param scope start of property name
692      * @return properties
693      */
694     public static synchronized Properties getSubProperties(String scope) {
695         final String keyStart;
696         if (scope == null || scope.isEmpty()) {
697             keyStart = "";
698         } else if (scope.endsWith(".")) {
699             keyStart = scope;
700         } else {
701             keyStart = scope + '.';
702         }
703         Properties result = new Properties();
704         for (Map.Entry<Object, Object> entry : SETTINGS_PROPERTIES.entrySet()) {
705             String key = (String) entry.getKey();
706             if (key.startsWith(keyStart)) {
707                 String value = (String) entry.getValue();
708                 result.setProperty(key.substring(keyStart.length()), value);
709             }
710         }
711         return result;
712     }
713 
714     /**
715      * Set Log4J logging level for the category
716      *
717      * @param category logging category
718      * @param level    logging level
719      */
720     public static synchronized void setLoggingLevel(String category, Level level) {
721         if (level != null) {
722             String prefix = getLoggingPrefix(category);
723             SETTINGS_PROPERTIES.setProperty(prefix + category, level.toString());
724             if ("rootLogger".equals(category)) {
725                 Logger.getRootLogger().setLevel(level);
726             } else {
727                 Logger.getLogger(category).setLevel(level);
728             }
729         }
730     }
731 
732     /**
733      * Change and save a single property.
734      *
735      * @param property property name
736      * @param value    property value
737      */
738     public static synchronized void saveProperty(String property, String value) {
739         Settings.load();
740         Settings.setProperty(property, value);
741         Settings.save();
742     }
743 
744     /**
745      * Test if running on Windows
746      *
747      * @return true on Windows
748      */
749     public static boolean isWindows() {
750         return System.getProperty("os.name").toLowerCase().startsWith("windows");
751     }
752 
753     /**
754      * Test if running on Linux
755      *
756      * @return true on Linux
757      */
758     public static boolean isLinux() {
759         return System.getProperty("os.name").toLowerCase().startsWith("linux");
760     }
761 
762     public static boolean isUnix() {
763         return isLinux() ||
764                 System.getProperty("os.name").toLowerCase().startsWith("freebsd");
765     }
766 
767     public static String getUserAgent() {
768         return getProperty("davmail.userAgent", Settings.EDGE_USER_AGENT);
769     }
770 
771     public static String getOutlookUrl() {
772         String tld = getProperty("davmail.tld");
773         String outlookUrl = getProperty("davmail.outlookUrl");
774         if (outlookUrl != null) {
775             return outlookUrl;
776         } else if (tld == null) {
777             return OUTLOOK_URL;
778         } else {
779             return "https://outlook.office365." + tld;
780         }
781     }
782 
783     public static String getO365Url() {
784         String tld = getProperty("davmail.tld");
785         String outlookUrl = getProperty("davmail.outlookUrl");
786         if (outlookUrl != null) {
787             return outlookUrl + "/EWS/Exchange.asmx";
788         } else if (tld == null) {
789             return O365_URL;
790         } else {
791             return "https://outlook.office365." + tld + "/EWS/Exchange.asmx";
792         }
793     }
794 
795     /**
796      * Handle custom graph endpoints.
797      * See <a href="https://learn.microsoft.com/en-us/graph/deployments">...</a>
798      * @return graph endpoint url
799      */
800     public static String getGraphUrl() {
801         String tld = getProperty("davmail.tld");
802         String graphUrl = getProperty("davmail.graphUrl");
803         if (graphUrl != null) {
804             return graphUrl;
805         } else if (tld == null) {
806             return GRAPH_URL;
807         } else {
808             return "https://graph.microsoft." + tld;
809         }
810     }
811 
812     public static String getO365LoginUrl() {
813         String tld = getProperty("davmail.tld");
814         String loginUrl = getProperty("davmail.loginUrl");
815         if (loginUrl != null) {
816             return loginUrl;
817         } else if (tld == null) {
818             return O365_LOGIN_URL;
819         } else {
820             return "https://login.microsoftonline." + tld;
821         }
822     }
823 
824     public static boolean isSWTAvailable() {
825         boolean isSWTAvailable = false;
826         ClassLoader classloader = Settings.class.getClassLoader();
827         try {
828             // trigger ClassNotFoundException
829             classloader.loadClass("org.eclipse.swt.SWT");
830             isSWTAvailable = true;
831         } catch (Throwable e) {
832             LOGGER.info(new BundleMessage("LOG_SWT_NOT_AVAILABLE"));
833         }
834         return isSWTAvailable;
835     }
836 
837     public static boolean isJFXAvailable() {
838         boolean isJFXAvailable = false;
839         try {
840             Class.forName("javafx.application.Platform");
841             isJFXAvailable = true;
842         } catch (ClassNotFoundException | NullPointerException e) {
843             LOGGER.info("JavaFX (OpenJFX) not available");
844         }
845         return isJFXAvailable;
846     }
847 
848     public static boolean isDocker() {
849         boolean isDocker = new File("/.dockerenv").exists();
850         if (isDocker) {
851             LOGGER.info("Running in docker");
852         }
853         return isDocker;
854     }
855 
856     public static boolean isFlatpak() {
857         boolean isFlatpak = "flatpak".equals(System.getenv("container"));
858         if (isFlatpak) {
859             LOGGER.info("Running in Flatpak");
860         }
861         return isFlatpak;
862     }
863 
864     /**
865      * Set davmail properties path in Docker and Flatpak
866      * @return davmail.properties path
867      */
868     public static synchronized String getConfigFilePath() {
869         if (isFlatpak()) {
870             return System.getenv("XDG_CONFIG_HOME")+"/davmail.properties";
871         } else {
872             // Docker
873             return System.getenv("DAVMAIL_PROPERTIES");
874         }
875     }
876 }