Outlook addin throwing errors I don't understand - email

I'm attempting to write an outlook-addin that reads appointments from a user's inbox and generates reminder emails 72 hours prior to the meeting. I learned how to do this by piecing together snippets from SO and the web, but I'm running into a few errors. I'm not sure I understand what's going on enough to even ask the correct questions, so please forgive my bumbling through this.
So, when I try to spoof the "Sender" using the code below, I get the following error:
"Generating server: MBX080-W3-CO-1.exch080.serverpod.net
josh.alcorn#crmpoint.net
Remote Server returned '550 5.6.2 STOREDRV.Submit; subscription not found'
Original message headers:
Received: from MBX080-W3-CO-1.exch080.serverpod.net ([10.224.117.52]) by
MBX080-W3-CO-1.exch080.serverpod.net ([169.254.1.30]) with mapi id
15.00.1044.021; Mon, 8 Jun 2015 08:28:25 -0700
MIME-Version: 1.0
Content-Type: text/plain
Date: Mon, 8 Jun 2015 08:28:25 -0700
X-MS-Exchange-Transport-FromEntityHeader: Hosted
Message-ID:
<b8690284ffb04af794a676b8efdee58d#MBX080-W3-CO-1.exch080.serverpod.net>
Subject: This is the subject"
WHen I remove the sender code, it seems to be firing correctly for 2 of my test appointments, but throws a nullreferenceexception on the third. I can't seem to figure out how to use the debugger in conjunction with Outlook so I don't have much better information on that error. As for this issue, any advice on how to debug an Outlook 2013 addin in real-time would be tremendously appreciated. I've tried all the tricks I know.
using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Outlook = Microsoft.Office.Interop.Outlook;
using Office = Microsoft.Office.Core;
using Microsoft.Office.Interop.Outlook;
using System.Windows.Forms;
//http://stackoverflow.com/questions/6425776/outlook-2010-how-to-get-a-list-of-all-appointments-including-recurrences
//https://www.add-in-express.com/creating-addins-blog/2013/06/10/outlook-calendar-appointment-meeting-items/
//http://www.sperrysoftware.com/outlook/email-reminders.asp
//myEmailAddress = this.ActiveExplorer().Session.CurrentUser.EmailAddress;
//Application.Session.CurrentUser.AddressEntry.Address
//https://www.add-in-express.com/creating-addins-blog/2013/06/10/outlook-calendar-appointment-meeting-items/#enumerate
//http://www.scrubly.com/blog/how-to-outlook/how-to-install-enable-and-disable-outlook-2013-add-ins/
//http://stackoverflow.com/questions/5472493/making-vsto-add-in-installable
//https://msdn.microsoft.com/en-us/library/cc442767.aspx#Download
namespace OutlookAddIn1
{
public partial class ThisAddIn
{
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
doStuff();
}
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
}
#region VSTO generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
//https://msdn.microsoft.com/en-us/library/ms268866.aspx
private void doStuff()
{
Thread.Sleep(120000); //120 seconds
DateTime firstRun = DateTime.Now; //So we can check every 24 hours? Maybe once initially as well.
DateTime lastRun = DateTime.Now;//.AddHours(1); //We're going to compare this to firstRun
bool whileTrue = true;
//int test = 0;
try
{
while (whileTrue)
{
if (whileTrue == true)//(firstRun > lastRun.AddDays(1))
{
Outlook.MAPIFolder calendarFolder = Application.Session.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderCalenda r);
Outlook.Items outlookCalendarItems = calendarFolder.Items;
outlookCalendarItems.IncludeRecurrences = false; //was true
List<Outlook.AppointmentItem> lst = new List<Outlook.AppointmentItem>();
foreach (Outlook.AppointmentItem item in outlookCalendarItems)
{
lst.Add(item);
//We can probably just handle logic in here without the second for loop that comes next
}
foreach (Outlook.AppointmentItem x in lst)
{
//http://stackoverflow.com/questions/16329581/getting-system-runtime-interopservices-comexception-error-in-server-side-generat
DateTime startDate = DateTime.Now.AddDays(1);
DateTime endDate = DateTime.Now.AddDays(5);
DateTime apptDate = x.Start;
if (x.Subject.ToLower().Contains("telos"))
{
MessageBox.Show("X: " + x.Start + "XYZ: " + x.Subject);
if (x.Start > startDate && x.Start < endDate)
{
Outlook.MailItem mailItem = (Outlook.MailItem)
this.Application.CreateItem(Outlook.OlItemType.olMailItem);
//http://stackoverflow.com/questions/11223462/how-to-send-a-mail-using-microsoft-office-interop-outlook-mailitem-by-specifying
//Outlook.Recipient recipient = this.Application.Session.CreateRecipient("someone#example.com");
//mailItem.Sender = recipient.AddressEntry;
Outlook.Recipient recipTo =
mailItem.Recipients.Add("someone#example.com");
recipTo.Type = (int)Outlook.OlMailRecipientType.olTo;
mailItem.Sender = (Outlook.AddressEntry)recipTo;
//Outlook.Account account = new Account();// Application.Session.Accounts["MyOtherAccount"];
//mailItem.SendUsingAccount = account;
mailItem.Subject = "This is the subject";
mailItem.To = Application.Session.CurrentUser.AddressEntry.Address; //"someone#example.com";
mailItem.Body = "This is the message.";
mailItem.Importance = Outlook.OlImportance.olImportanceLow;
mailItem.Display(false);
((Outlook._MailItem)mailItem).Send();
//mailItem.Send();
/*
//Here we generate the email
Outlook.Application app = new Outlook.Application();
Microsoft.Office.Interop.Outlook.MailItem email = app.CreateItem((OlItemType.olMailItem));
Outlook.Recipient recipient = app.Session.CreateRecipient("someone#example.com");
email.Sender = recipient.AddressEntry;
email.Display(false);
email.Subject = "You have a new appointment";
email.To = Application.Session.CurrentUser.AddressEntry.Address; //Current email address.
email.Body = "This email was automatically generated to remind you have an upcoming appointment on: " + x.Start.ToString();
((Outlook._MailItem)email).Send();
//email.Send();
//((Outlook._MailItem)mailItem).Send();
app.Quit();
* */
}
}
}
lastRun = DateTime.Now;
whileTrue = false;
}
else
{
}
}
}
catch (System.Exception e) //Microsoft.Office.Interop.Outlook.Exception e
{
MessageBox.Show(e.ToString());
}
}
public void createContact()
{
Outlook.ContactItem newContact = (Outlook.ContactItem)
this.Application.CreateItem(Outlook.OlItemType.olContactItem);
try
{
newContact.FirstName = "Person";
newContact.LastName = "LastName";
newContact.Email1Address = "someone#example.com";
newContact.Save();
newContact.Display(true);
}
catch
{
MessageBox.Show("The new contact was not saved.");
}
}
#endregion
}
}

Running a time-consuming task in the Startup event handler is a good idea. The fact is that Outlook measures the time required for loading add-ins and may disable it automatically (if it takes a lot of time). See the Performance criteria for keeping add-ins enabled section in the What's new for Outlook 2013 developers article in MSDN for more information.
Also I'd suggest creating a log file and writing any action in the log. Thus, you will be aware what happens at runtime and what line of code generates an error.

Related

Unity Mirror - NetworkServer Send Message To Target Client

I'm not sure what I'm doing wrong here but I can't seem to get my message from the server to the client. Here is what I have so far:
protected virtual void RegisterHandlers(bool enable)
{
if (enable)
{
NetworkServer.RegisterHandler<ClientRequestLoadScene>(OnClientRequestedToLoadScene);
NetworkClient.RegisterHandler<ServerRequestLoadScene>(OnServerRequestLoadScene);
}
else
{
NetworkServer.UnregisterHandler<ClientRequestLoadScene>();
NetworkClient.UnregisterHandler<ServerRequestLoadScene>();
}
}
The above is called when the instance starts to register a new handler. Then I have the client call:
ClientRequestLoadScene msg = new ClientRequestLoadScene();
msg.scene = scene;
NetworkClient.Send(msg);
This is received by the server fine. Then the server runs the following:
private void OnClientRequestedToLoadScene(NetworkConnection conn, ClientRequestLoadScene msg)
{
...
...
ServerRequestLoadScene server_msg = new ServerRequestLoadScene();
server_msg.scene = msg.scene;
NetworkServer.SendToClientOfPlayer(conn.identity, msg);
...
...
}
The above message is never received by the client. I have also tried: NetworkServer.SendToAll(msg); and that is never received by the client either. What am I doing wrong?
The issue with the above is with these lines:
server_msg.scene = msg.scene;
NetworkServer.SendToClientOfPlayer(conn.identity, msg);
It needed to be:
server_msg.scene = msg.scene;
conn.Send(server_msg);

OPC UA Client capture the lost item values from the UA server after a disconnect/connection error?

I am building a OPC UA Client using OPC Foundation SDK. I am able to create a subscription containing some Monitoreditems.
On the OPC UA server these monitored items change value constantly (every second or so).
I want to disconnect the client (simulate a connection broken ), keep the subcription alive and wait for a while. Then I reconnect having my subscriptions back, but I also want all the monitored Item values queued up during the disconnect. Right now I only get the last server value on reconnect.
I am setting a queuesize:
monitoredItem.QueueSize = 100;
To kind of simulate a connection error I have set the "delete subscription" to false on ClosesSession:
m_session.CloseSession(new RequestHeader(), false);
My question is how to capture the content of the queue after a disconnect/connection error???
Should the ‘lost values’ be “new MonitoredItem_Notification” automatically when the client reconnect?
Should the SubscriptionId be the same as before the connection was broken?
Should the sessionId be the same or will a new SessionId let med keep the existing subscriptions? What is the best way to simulate a connection error?
Many questions :-)
A sample from the code where I create the subscription containing some MonitoredItems and the MonitoredItem_Notification event method.
Any OPC UA Guru out there??
if (node.Displayname == "node to monitor")
{
MonitoredItem mon = CreateMonitoredItem((NodeId)node.reference.NodeId, node.Displayname);
m_subscription.AddItem(mon);
m_subscription.ApplyChanges();
}
private MonitoredItem CreateMonitoredItem(NodeId nodeId, string displayName)
{
if (m_subscription == null)
{
m_subscription = new Subscription(m_session.DefaultSubscription);
m_subscription.PublishingEnabled = true;
m_subscription.PublishingInterval = 3000;//1000;
m_subscription.KeepAliveCount = 10;
m_subscription.LifetimeCount = 10;
m_subscription.MaxNotificationsPerPublish = 1000;
m_subscription.Priority = 100;
bool cache = m_subscription.DisableMonitoredItemCache;
m_session.AddSubscription(m_subscription);
m_subscription.Create();
}
// add the new monitored item.
MonitoredItem monitoredItem = new MonitoredItem(m_subscription.DefaultItem);
//Each time a monitored item is sampled, the server evaluates the sample using a filter defined for each monitoreditem.
//The server uses the filter to determine if the sample should be reported. The type of filter is dependent on the type of item.
//DataChangeFilter for Variable, Eventfilter when monitoring Events. etc
//MonitoringFilter f = new MonitoringFilter();
//DataChangeFilter f = new DataChangeFilter();
//f.DeadbandValue
monitoredItem.StartNodeId = nodeId;
monitoredItem.AttributeId = Attributes.Value;
monitoredItem.DisplayName = displayName;
//Disabled, Sampling, (Report (includes sampling))
monitoredItem.MonitoringMode = MonitoringMode.Reporting;
//How often the Client wish to check for new values on the server. Must be 0 if item is an event.
//If a negative number the SamplingInterval is set equal to the PublishingInterval (inherited)
//The Subscriptions KeepAliveCount should always be longer than the SamplingInterval/PublishingInterval
monitoredItem.SamplingInterval = 500;
//Number of samples stored on the server between each reporting
monitoredItem.QueueSize = 100;
monitoredItem.DiscardOldest = true;//Discard oldest values when full
monitoredItem.CacheQueueSize = 100;
monitoredItem.Notification += m_MonitoredItem_Notification;
if (ServiceResult.IsBad(monitoredItem.Status.Error))
{
return null;
}
return monitoredItem;
}
private void MonitoredItem_Notification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new MonitoredItemNotificationEventHandler(MonitoredItem_Notification), monitoredItem, e);
return;
}
try
{
if (m_session == null)
{
return;
}
MonitoredItemNotification notification = e.NotificationValue as MonitoredItemNotification;
if (notification == null)
{
return;
}
string sess = m_session.SessionId.Identifier.ToString();
string s = string.Format(" MonitoredItem: {0}\t Value: {1}\t Status: {2}\t SourceTimeStamp: {3}", monitoredItem.DisplayName, (notification.Value.WrappedValue.ToString().Length == 1) ? notification.Value.WrappedValue.ToString() : notification.Value.WrappedValue.ToString(), notification.Value.StatusCode.ToString(), notification.Value.SourceTimestamp.ToLocalTime().ToString("HH:mm:ss.fff"));
richTextBox1.AppendText(s + "SessionId: " + sess);
}
catch (Exception exception)
{
ClientUtils.HandleException(this.Text, exception);
}
}e here
I don't know how much of this, if any, the SDK you're using does for you, but the approach when reconnecting is generally:
try to resume (re-activate) your old session. If this is successful your subscriptions will already exist and all you need to do is send more PublishRequests. Since you're trying to test by closing the session this probably won't work.
create a new session and then call the TransferSubscription service to transfer the previous subscriptions to your new session. You can then start sending PublishRequests and you'll get the queued notifications.
Again, depending on the stack/SDK/toolkit you're using some or none of this may be handled for you.

Why doesn't the NetworkUp event fire on the .Net Gadgeteer GHI WiFi RS21 module?

I'm trying to debug why the event NetworkUp never fires on the WiFi RS21 Gadgeteer module and I've distilled it down to a very simple code listing:
using Microsoft.SPOT;
using Gadgeteer.Networking;
using GT = Gadgeteer;
using GTM = Gadgeteer.Modules;
namespace NetworkUpTest
{
public partial class Program
{
void ProgramStarted()
{
wifi_RS21.UseDHCP();
wifi_RS21.NetworkUp += wifi_RS21_NetworkUp;
wifi_RS21.NetworkDown += wifi_RS21_NetworkDown;
var scans = wifi_RS21.Interface.Scan("LLOYDREGANS");
if (scans != null && scans.Length > 0)
{
Debug.Print("Joining " + scans[0].SSID);
wifi_RS21.Interface.Join(scans[0], "**********");
}
var giveUpWaitingForTheNetworkUpEvent = new GT.Timer(300000, GT.Timer.BehaviorType.RunOnce);
giveUpWaitingForTheNetworkUpEvent.Tick += giveUpWaitingForTheNetworkUpEvent_Tick;
giveUpWaitingForTheNetworkUpEvent.Start();
}
void wifi_RS21_NetworkUp(GTM.Module.NetworkModule sender, GTM.Module.NetworkModule.NetworkState state)
{
Debug.Print("NetworkUp");
}
void wifi_RS21_NetworkDown(GTM.Module.NetworkModule sender, GTM.Module.NetworkModule.NetworkState state)
{
Debug.Print("NetworkDown");
}
void giveUpWaitingForTheNetworkUpEvent_Tick(GT.Timer timer)
{
Debug.Print("Give up waiting for the NetworkUp event and try requesting the router homepage");
var request = HttpHelper.CreateHttpGetRequest("http://192.168.1.1/");
request.ResponseReceived += request_ResponseReceived;
request.SendRequest();
}
void request_ResponseReceived(HttpRequest sender, HttpResponse response)
{
Debug.Print("Response received. response.Text.Length = " + response.Text.Length);
}
}
}
Here's the listing from the output window when the program runs (minus the thread exited reports):
Using mainboard GHI Electronics FEZSpider version 1.0
RS9110 firmware version Number is 4.4.5
RS9110 driver version Number is 4.4.5
Joining LLOYDREGANS
NetworkDown
Give up waiting for the NetworkUp event and try requesting the router homepage
Response received. response.Text.Length = 2509
Given that the network is demonstrably up, why is "NetworkDown" the only event that fires from the WiFi RS21 module?
The answer on the GHI forum suggests that the correct events to use are:
Interface.WirelessConnectivityChanged
Interface.NetworkAddressChanged
instead of the exemplar code on GHI's WiFi RS21 Gadgeteer module page.

Test Event expiration in Drools Fusion CEP

Ciao, I have tested in several ways, but I'm still unable to test and verify the Event expiration mechanism in Drools Fusion, so I'm looking for some little guidance, please?
I've read the manual and I'm interested in this feature:
In other words, one an event is inserted into the working memory, it is possible for the engine to find out when an event can no longer match other facts and automatically retract it, releasing its associated resources.
I'm using the Drools IDE in Eclipse, 5.4.0.Final and I modified the template code created by the "New Drools Project" wizard to test and verify for Event expiration.
The code below. The way I understood to make the "lifecycle" to work correctly is that:
You must setup the KBase in STREAM mode - check
You must Insert the Events in temporal order - check
You must define temporal constraints between Events - check in my case is last Message()
However, when I inspect the EventFactHandle at the end, none of the Event() has expired.
Thanks for your help.
Java:
public class DroolsTest {
public static final void main(String[] args) {
try {
KnowledgeBase kbase = readKnowledgeBase();
// I do want the pseudo clock
KnowledgeSessionConfiguration conf = KnowledgeBaseFactory.newKnowledgeSessionConfiguration();
conf.setOption(ClockTypeOption.get("pseudo"));
StatefulKnowledgeSession ksession = kbase.newStatefulKnowledgeSession(conf, null);
SessionPseudoClock clock = ksession.getSessionClock();
KnowledgeRuntimeLogger logger = KnowledgeRuntimeLoggerFactory.newFileLogger(ksession, "test");
// Insert of 2 Event:
Message message = new Message();
message.setMessage("Message 1");
message.setStatus(Message.HELLO);
ksession.insert(message);
ksession.fireAllRules();
clock.advanceTime(1, TimeUnit.DAYS);
Message message2 = new Message();
message2.setMessage("Message 2");
message2.setStatus(Message.HELLO);
ksession.insert(message2);
ksession.fireAllRules();
clock.advanceTime(1, TimeUnit.DAYS);
ksession.fireAllRules();
// Now I do check what I have in the working memory and if EventFactHandle if it's expired or not:
for (FactHandle f : ksession.getFactHandles()) {
if (f instanceof EventFactHandle) {
System.out.println(((EventFactHandle)f)+" "+((EventFactHandle)f).isExpired());
} else {
System.out.println("not an Event: "+f);
}
}
logger.close();
} catch (Throwable t) {
t.printStackTrace();
}
}
private static KnowledgeBase readKnowledgeBase() throws Exception {
KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder();
kbuilder.add(ResourceFactory.newClassPathResource("Sample.drl"), ResourceType.DRL);
KnowledgeBuilderErrors errors = kbuilder.getErrors();
if (errors.size() > 0) {
for (KnowledgeBuilderError error: errors) {
System.err.println(error);
}
throw new IllegalArgumentException("Could not parse knowledge.");
}
KnowledgeBase kbase = KnowledgeBaseFactory.newKnowledgeBase();
kbase.addKnowledgePackages(kbuilder.getKnowledgePackages());
// following 2 lines is the template code modified for STREAM configuration
KnowledgeBaseConfiguration config = KnowledgeBaseFactory.newKnowledgeBaseConfiguration();
config.setOption( EventProcessingOption.STREAM );
return kbase;
}
/*
* This is OK from template, as from the doc:
* By default, the timestamp for a given event is read from the Session Clock and assigned to the event at the time the event is inserted into the working memory.
*/
public static class Message {
public static final int HELLO = 0;
public static final int GOODBYE = 1;
private String message;
private int status;
public String getMessage() {
return this.message;
}
public void setMessage(String message) {
this.message = message;
}
public int getStatus() {
return this.status;
}
public void setStatus(int status) {
this.status = status;
}
}
}
Drools:
package com.sample
import com.sample.DroolsTest.Message;
declare Message
#role(event)
end
declare window LastMessageWindow
Message() over window:length(1)
end
rule "Hello World"
when
accumulate( $m : Message(status==Message.HELLO) from window LastMessageWindow,
$messages : collectList( $m ) )
then
System.out.println( ((Message)$messages.get(0)).getMessage() );
end
Please note: even if I add expiration of 1second to the Message event, by
#expires(1s)
I still don't get the expected result that the very first Message event inserted, I would have expected is now expired? Thanks for your help.
Found solution! Obviously it was me being stupid and not realizing I was using Drools 5.4.0.Final while still referring to old documentation of 5.2.0.Final. In the updated documentation for Drools Fusion 5.4.0.Final, this box is added for 2.6.2. Sliding Length Windows:
Please note that length based windows do not define temporal constraints for event expiration from the session, and the engine will not consider them. If events have no other rules defining temporal constraints and no explicit expiration policy, the engine will keep them in the session indefinitely.
Therefore the 3rd requirement I originally enlisted of "You must define temporal constraints between Events" is obviously NOT met because I now understand Sliding Length Window in Drools 5.4.0.Final:
Message() over window:length(1)
are indeed NOT a definition of a temporal constraints for event expiration from the session.
Updating this answer hopefully somebody will find it helpful. Also, just so for your know, me being stupid actually for relying on googling in order to reach the doc, and sometimes you don't get redirected to the current release documentation, so it seems...

Quartz Scheduler sending email notification multiple time

I am using Quartz for scheduling job. Job is to send reminder email every day at some particular time say 11:00AM. I am able to send reminder mail successfully, but the problem is that it sends more than 1 mails at same time. Sometime it sends 8 mails for 1 reminder request, sometime it sends 5. It seems same job is executed multiple time.
Following is my code,
JobDetail job = JobBuilder.newJob(LmsJob.class)
.withIdentity("lmsJob", org.quartz.Scheduler.DEFAULT_GROUP)
.build();
JobDataMap map = job.getJobDataMap();
map.put("creditMonthlyLeaveBalance", creditMonthlyLeaveBalance);
map.put("dailyUpdationTask", dailyUpdation);
map.put("monthlyPayrollGenerationTask",
monthlyPayrollGenerationTask);
map.put("yearlyMaintenanceOfLeaveBalance",
yearlyMaintenanceOfLeaveBalance);
map.put("emailNotifier", emailNotifier);
try {
CronTrigger trigger = TriggerBuilder
.newTrigger()
.withIdentity("lmsJob", "lmsJobGroup")
.forJob(job)
.startAt(new Date(System.currentTimeMillis()))
.withSchedule(
CronScheduleBuilder
.cronSchedule("00 00 00 ? * *")).build();
scheduler.scheduleJob(job, trigger);
scheduler.start();
// scheduler.shutdown();
} catch (ParseException e) {
e.printStackTrace();
}
Please help me in this, let me know if anything else is needed from my side.
I do not know your entire code,which annotation you gave and stuff.So, I am guessing that you gave annotation like #QuartzEvery("3h"). As far I am guessing, your job is scheduled wrong.To make it run at a particular time of every day,try this...
QuartzManager implements Managed {
.
.
public void start() throws Exception {
.
.
QuartzDailyAt dailyAt = jobType.getAnnotation(QuartzDailyAt.class);
int hours[] = dailyAt.hours();
String hourString =
Arrays.stream(hours).mapToObj(String::valueOf).collect(joining(","));
String cronExpression = String.format("0 0 %s * * ?", hourString);
Trigger trigger = TriggerBuilder.
newTrigger().
startNow().
withSchedule(CronScheduleBuilder.cronSchedule(cronExpression).
inTimeZone(TimeZone.getTimeZone("IST"))).
build();
scheduler.scheduleJob(JobBuilder.newJob(jobType).build(), trigger);
scheduler.start();
.
.
}
.
}
And interface as
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
public #interface QuartzDailyAt {
int[] hours();
}
While running your job,add an annotation at the top of the class like
#QuartzDailyAt(hours = {7,8,9,15,16,17})
public class SomeJob extends QuartzJob {.....}
This gives you to run at every particular intervals of a particular time zone...(above it is IST)