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